From 633b4c2b4e0360e34c444836e454655ed434bd67 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 12:39:45 -0400 Subject: [PATCH 1/4] perf(compose): add stability configuration for non-Compose model types Adds compose_compiler_config.conf and wires it into both the library Compose convention plugin and the app module. This tells the Compose compiler to treat key model types from non-Compose modules as stable, enabling skipping optimizations for composables that accept them. Covered types: - Solana key hierarchy (Key32, PublicKey, Mint) - Financial models (Fiat, Rate, LocalFiat, MintMetadata, TokenWithBalance, TokenWithLocalizedBalance, etc.) - Social links (sealed interface + subtypes) - Bill customizations (contains ByteArray with custom equals) - JDK/Kotlin time types (java.time.Instant, kotlin.time.Instant) --- apps/flipcash/app/build.gradle.kts | 6 +++ .../AndroidLibraryComposeConventionPlugin.kt | 7 ++++ compose_compiler_config.conf | 38 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 compose_compiler_config.conf diff --git a/apps/flipcash/app/build.gradle.kts b/apps/flipcash/app/build.gradle.kts index eb69774cc..f238e4711 100644 --- a/apps/flipcash/app/build.gradle.kts +++ b/apps/flipcash/app/build.gradle.kts @@ -118,6 +118,12 @@ bugsnag { } } +composeCompiler { + stabilityConfigurationFile.set( + rootProject.layout.projectDirectory.file("compose_compiler_config.conf") + ) +} + kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(libs.versions.android.java.get())) diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index eaa1861ef..c6e115c01 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -5,6 +5,7 @@ import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension class AndroidLibraryComposeConventionPlugin : Plugin { override fun apply(target: Project) { @@ -20,6 +21,12 @@ class AndroidLibraryComposeConventionPlugin : Plugin { } } + extensions.configure { + stabilityConfigurationFile.set( + rootProject.layout.projectDirectory.file("compose_compiler_config.conf") + ) + } + val libs = extensions.getByType().named("libs") dependencies { diff --git a/compose_compiler_config.conf b/compose_compiler_config.conf new file mode 100644 index 000000000..7e4a331bd --- /dev/null +++ b/compose_compiler_config.conf @@ -0,0 +1,38 @@ +// Compose Compiler Stability Configuration +// Types listed here are treated as stable by the Compose compiler, +// enabling skipping optimizations for composables that accept them. +// +// Only types from non-Compose modules (where @Stable/@Immutable cannot +// be applied directly) belong here. + +// Kotlin / JDK time types +kotlin.time.Instant +java.time.Instant + +// Solana key hierarchy (open class chain with List internals) +com.getcode.solana.keys.Key32 +com.getcode.solana.keys.PublicKey +com.getcode.solana.keys.Mint + +// Financial model types (services/opencode — no Compose dependency) +com.getcode.opencode.model.financial.Fiat +com.getcode.opencode.model.financial.Rate +com.getcode.opencode.model.financial.LocalFiat +com.getcode.opencode.model.financial.MintMetadata +com.getcode.opencode.model.financial.TokenWithBalance +com.getcode.opencode.model.financial.TokenWithLocalizedBalance +com.getcode.opencode.model.financial.VmMetadata +com.getcode.opencode.model.financial.LaunchpadMetadata +com.getcode.opencode.model.financial.HolderMetrics +com.getcode.opencode.model.financial.HolderMetrics.HolderDelta +com.getcode.opencode.model.financial.SocialLink +com.getcode.opencode.model.financial.SocialLink.Website +com.getcode.opencode.model.financial.SocialLink.X +com.getcode.opencode.model.financial.SocialLink.Telegram +com.getcode.opencode.model.financial.SocialLink.Discord + +// Bill customization (contains ByteArray, custom equals) +com.getcode.opencode.model.ui.TokenBillCustomizations + +// Legacy model types (libs/models — no Compose dependency) +com.getcode.model.Fiat From 192447938e4774135689dc0ab3e5302ce6e8e230 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 12:59:37 -0400 Subject: [PATCH 2/4] refactor(compose): remove legacy Fiat, KinAmount, and related dead types Delete com.getcode.model.Fiat, GenericAmount, SocialUser, TwitterUser from libs/models and KinAmount + KinAmountExt from libs/currency. Stub chat Exchange/MessageTip/payment code with TODO(chat-v2) comments for reimplementation with OCP Fiat. Remove KinAmount overloads from PriceWithFlag and Currency.format. Clean up stability config. --- compose_compiler_config.conf | 2 - .../com/getcode/extensions/KinAmountExt.kt | 49 ----------- .../kotlin/com/getcode/model/KinAmount.kt | 60 -------------- .../main/kotlin/com/getcode/utils/Currency.kt | 9 -- .../src/main/kotlin/com/getcode/model/Fiat.kt | 82 ------------------- .../kotlin/com/getcode/model/SocialUser.kt | 20 ----- .../kotlin/com/getcode/model/TwitterUser.kt | 39 --------- .../com/getcode/model/chat/MessageContent.kt | 15 ++-- .../getcode/ui/components/PriceWithFlag.kt | 22 ----- .../chat/messagecontents/Feedback.kt | 6 +- .../chat/messagecontents/MessagePayment.kt | 28 +++---- .../ui/components/chat/utils/ChatItem.kt | 4 +- .../ui/components/chat/utils/LocalizedText.kt | 61 +------------- 13 files changed, 29 insertions(+), 368 deletions(-) delete mode 100644 libs/currency/src/main/kotlin/com/getcode/extensions/KinAmountExt.kt delete mode 100644 libs/currency/src/main/kotlin/com/getcode/model/KinAmount.kt delete mode 100644 libs/models/src/main/kotlin/com/getcode/model/Fiat.kt delete mode 100644 libs/models/src/main/kotlin/com/getcode/model/SocialUser.kt delete mode 100644 libs/models/src/main/kotlin/com/getcode/model/TwitterUser.kt diff --git a/compose_compiler_config.conf b/compose_compiler_config.conf index 7e4a331bd..6346a4cd2 100644 --- a/compose_compiler_config.conf +++ b/compose_compiler_config.conf @@ -34,5 +34,3 @@ com.getcode.opencode.model.financial.SocialLink.Discord // Bill customization (contains ByteArray, custom equals) com.getcode.opencode.model.ui.TokenBillCustomizations -// Legacy model types (libs/models — no Compose dependency) -com.getcode.model.Fiat diff --git a/libs/currency/src/main/kotlin/com/getcode/extensions/KinAmountExt.kt b/libs/currency/src/main/kotlin/com/getcode/extensions/KinAmountExt.kt deleted file mode 100644 index bc5ce4e08..000000000 --- a/libs/currency/src/main/kotlin/com/getcode/extensions/KinAmountExt.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.getcode.extensions - -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import com.getcode.libs.currency.R -import com.getcode.model.Currency -import com.getcode.model.KinAmount -import com.getcode.util.resources.AndroidResources -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.FormatUtils -import com.getcode.utils.Kin -import com.getcode.utils.LocalCurrencyUtils -import com.getcode.utils.formatAmountString - -fun KinAmount.formattedRaw() = FormatUtils.formatWholeRoundDown(kin.toKin().toDouble()) - -@Composable -fun KinAmount.formatted( - currency: Currency, - amount: Double = fiat, - suffix: String = stringResource(R.string.core_ofKin) -) = formatAmountString( - resources = AndroidResources(context = LocalContext.current), - currency = currency, - amount = amount, - suffix = suffix -) - -@Composable -fun KinAmount.formatted(amount: Double = fiat, suffix: String = stringResource(R.string.core_ofKin)): String { - val currency = LocalCurrencyUtils.current?.getCurrency(rate.currency.name) - ?: Currency.Kin - return formatted(currency = currency, amount = amount, suffix = suffix) -} - -fun KinAmount.formatted( - resources: ResourceHelper, - currency: Currency, - amount: Double = fiat, - kinSuffix: String = "", - suffix: String = resources.getString(R.string.core_ofKin) -) = formatAmountString( - resources = resources, - currency = currency, - amount = amount, - kinSuffix = kinSuffix, - suffix = suffix -) \ No newline at end of file diff --git a/libs/currency/src/main/kotlin/com/getcode/model/KinAmount.kt b/libs/currency/src/main/kotlin/com/getcode/model/KinAmount.kt deleted file mode 100644 index 6a07830ca..000000000 --- a/libs/currency/src/main/kotlin/com/getcode/model/KinAmount.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.getcode.model - -import android.os.Parcelable -import com.getcode.model.Kin.Companion.fromKin -import com.getcode.utils.serializer.KinQuarksSerializer -import com.getcode.utils.serializer.RateAsStringSerializer -import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable - -@Serializable -@Parcelize -data class KinAmount( - @Serializable(with = KinQuarksSerializer::class) - val kin: Kin, - val fiat: Double, - @Serializable(with = RateAsStringSerializer::class) - val rate: Rate -): Parcelable { - fun truncating() = KinAmount( - kin = kin.toKinTruncating(), - fiat = fiat, - rate = rate - ) - - fun replacing(rate: Rate): KinAmount { - return newInstance(this.kin, rate) - } - - companion object { - val Zero = newInstance(0, Rate.oneToOne) - - fun newInstance(kin: Long, rate: Rate): KinAmount { - return newInstance(fromKin(kin), rate) - } - - fun fromQuarks(quarks: Long): KinAmount = newInstance(quarks, Rate.oneToOne) - - fun newInstance(kin: Kin, rate: Rate): KinAmount { - return KinAmount( - kin = kin, - fiat = kin.toFiat(fx = rate.fx), - rate = rate - ) - } - } -} - -fun List.sum(): KinAmount { - if (this.isEmpty()) return KinAmount.Zero - - val totalKin = this.fold(Kin(0)) { acc, kinAmount -> acc + kinAmount.kin } - val totalFiat = this.sumOf { it.fiat } - val rate = this.first().rate // Assuming the rate is consistent or using the first item's rate - - return KinAmount( - kin = totalKin, - fiat = totalFiat, - rate = rate - ) -} \ No newline at end of file diff --git a/libs/currency/src/main/kotlin/com/getcode/utils/Currency.kt b/libs/currency/src/main/kotlin/com/getcode/utils/Currency.kt index e19592c86..7ff35f732 100644 --- a/libs/currency/src/main/kotlin/com/getcode/utils/Currency.kt +++ b/libs/currency/src/main/kotlin/com/getcode/utils/Currency.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable import com.getcode.libs.currency.R import com.getcode.model.Currency import com.getcode.model.CurrencyCode -import com.getcode.model.KinAmount import com.getcode.util.resources.LocalResources import com.getcode.util.resources.ResourceHelper import com.getcode.util.resources.ResourceType @@ -51,14 +50,6 @@ private fun getFlag(resourceHelper: ResourceHelper, countryCode: String): Int? { ) } -fun Currency.format(resources: ResourceHelper, amount: KinAmount): String { - return formatAmountString( - resources, - this, - amount.fiat - ) -} - fun Currency.format(resources: ResourceHelper, amount: Double): String { return formatAmountString( resources, diff --git a/libs/models/src/main/kotlin/com/getcode/model/Fiat.kt b/libs/models/src/main/kotlin/com/getcode/model/Fiat.kt deleted file mode 100644 index e35cb756b..000000000 --- a/libs/models/src/main/kotlin/com/getcode/model/Fiat.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.getcode.model - -import kotlinx.serialization.Serializable - - - -@Serializable -data class Fiat( - val currency: CurrencyCode, - val amount: Double, -): Value { - - companion object { - fun fromString(currency: CurrencyCode, amountString: String): Fiat? { - val amount = amountString.toDoubleOrNull() ?: return null - return Fiat( - currency = currency, - amount = amount - ) - } - } -} - -@Serializable -sealed interface GenericAmount { - - val currencyCode: CurrencyCode - - @Serializable - data class Exact(val amount: KinAmount): GenericAmount { - override val currencyCode: CurrencyCode = amount.rate.currency - } - - @Serializable - data class Partial(val fiat: Fiat): GenericAmount { - override val currencyCode: CurrencyCode = fiat.currency - } - - fun amountUsing(rate: Rate): KinAmount { - return when (this) { - is Exact -> amount - is Partial -> KinAmount.fromFiatAmount(fiat.amount, rate) - } - } -} - -fun KinAmount.Companion.fromFiatAmount(kin: Kin, fiat: Double, fx: Double, currencyCode: CurrencyCode): KinAmount { - return KinAmount( - kin = kin.inflating(), - fiat = fiat, - rate = Rate( - fx = fx, - currency = currencyCode - ) - ) -} - -fun KinAmount.Companion.fromFiatAmount(fiat: Double, fx: Double, currencyCode: CurrencyCode): KinAmount { - return fromFiatAmount( - kin = Kin.fromFiat(fiat = fiat, fx = fx), - fiat = fiat, - fx = fx, - currencyCode = currencyCode - ) -} - -fun KinAmount.Companion.fromFiatAmount(fiat: Double, rate: Rate): KinAmount { - return fromFiatAmount( - kin = Kin.fromFiat(fiat = fiat, fx = rate.fx), - fiat = fiat, - fx = rate.fx, - currencyCode = rate.currency - ) -} - -fun KinAmount.Companion.fromFiatAmount(fiat: Fiat, rate: Rate): KinAmount { - return KinAmount( - kin = Kin.fromFiat(fiat = fiat.amount, fx = rate.fx), - fiat = fiat.amount, - rate = rate - ) -} \ No newline at end of file diff --git a/libs/models/src/main/kotlin/com/getcode/model/SocialUser.kt b/libs/models/src/main/kotlin/com/getcode/model/SocialUser.kt deleted file mode 100644 index d3baeadb6..000000000 --- a/libs/models/src/main/kotlin/com/getcode/model/SocialUser.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.getcode.model - -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.serializer.PublicKeyAsStringSerializer -import kotlinx.serialization.Serializable - -@Serializable -sealed interface SocialUser { - val platform: String - val username: String - @Serializable(with = PublicKeyAsStringSerializer::class) - val tipAddress: PublicKey - val imageUrl: String? - - val imageUrlSanitized: String? - - val costOfFriendship: Fiat - val isFriend: Boolean - val chatId: ID -} \ No newline at end of file diff --git a/libs/models/src/main/kotlin/com/getcode/model/TwitterUser.kt b/libs/models/src/main/kotlin/com/getcode/model/TwitterUser.kt deleted file mode 100644 index 769f465ae..000000000 --- a/libs/models/src/main/kotlin/com/getcode/model/TwitterUser.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.getcode.model - -import android.webkit.MimeTypeMap -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.serializer.PublicKeyAsStringSerializer -import kotlinx.serialization.Serializable - -@Serializable -data class TwitterUser( - override val username: String, - @Serializable(with = PublicKeyAsStringSerializer::class) - override val tipAddress: PublicKey, - override val imageUrl: String?, - val displayName: String, - val followerCount: Int, - val verificationStatus: VerificationStatus, - override val costOfFriendship: Fiat, - override val isFriend: Boolean, - override val chatId: ID, -): SocialUser { - - override val platform: String = "X" - - override val imageUrlSanitized: String? - get() { - val url = imageUrl ?: return null - val extension = MimeTypeMap.getFileExtensionFromUrl(url) - val urlWithoutExtension = url.removeSuffix(extension) - val urlWithoutType = urlWithoutExtension.substringBeforeLast("_") - - return urlWithoutType.plus(".$extension") - } - - enum class VerificationStatus { - none, blue, business, government, unknown - } - - companion object -} \ No newline at end of file diff --git a/libs/models/src/main/kotlin/com/getcode/model/chat/MessageContent.kt b/libs/models/src/main/kotlin/com/getcode/model/chat/MessageContent.kt index 7a8b95006..aa324c8f4 100644 --- a/libs/models/src/main/kotlin/com/getcode/model/chat/MessageContent.kt +++ b/libs/models/src/main/kotlin/com/getcode/model/chat/MessageContent.kt @@ -1,7 +1,7 @@ package com.getcode.model.chat +import com.getcode.model.CurrencyCode import com.getcode.model.EncryptedData -import com.getcode.model.GenericAmount import com.getcode.model.ID import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -86,9 +86,11 @@ sealed interface MessageContent { override val content: String = value } + // TODO(chat-v2): Exchange message amounts will use com.getcode.opencode.model.financial.Fiat @Serializable data class Exchange( - val amount: GenericAmount, + val amount: Double, + val currencyCode: CurrencyCode, val verb: Verb, val reference: Reference?, override val isFromSelf: Boolean, @@ -97,6 +99,7 @@ sealed interface MessageContent { override fun hashCode(): Int { var result = amount.hashCode() + result += currencyCode.hashCode() result += verb.hashCode() result += (reference?.hashCode() ?: 0) result += isFromSelf.hashCode() @@ -112,6 +115,7 @@ sealed interface MessageContent { other as Exchange if (amount != other.amount) return false + if (currencyCode != other.currencyCode) return false if (verb != other.verb) return false if (reference != other.reference) return false if (isFromSelf != other.isFromSelf) return false @@ -122,13 +126,14 @@ sealed interface MessageContent { @Serializable internal data class Content( - val amount: GenericAmount, + val amount: Double, + val currencyCode: CurrencyCode, val verb: Verb, val reference: Reference?, ) @Transient - override val content: String = Json.encodeToString(Content(amount, verb, reference)) + override val content: String = Json.encodeToString(Content(amount, currencyCode, verb, reference)) } @Serializable @@ -509,7 +514,7 @@ sealed interface MessageContent { 1 -> RawText(content, isFromSelf) 2 -> { val data = Json.decodeFromString(content) - Exchange(data.amount, data.verb, data.reference, isFromSelf) + Exchange(data.amount, data.currencyCode, data.verb, data.reference, isFromSelf) } 3 -> { val data = Json.decodeFromString(content) diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/PriceWithFlag.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/PriceWithFlag.kt index d8f7d0025..af9676d79 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/PriceWithFlag.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/PriceWithFlag.kt @@ -15,11 +15,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp -import com.getcode.model.CurrencyCode -import com.getcode.model.KinAmount import com.getcode.theme.CodeTheme -import com.getcode.utils.flagResId -import com.getcode.extensions.formatted object PriceWithFlagDefaults { @@ -33,24 +29,6 @@ object PriceWithFlagDefaults { } } -@Composable -fun PriceWithFlag( - modifier: Modifier = Modifier, - currencyCode: CurrencyCode, - amount: KinAmount, - iconSize: Dp = CodeTheme.dimens.staticGrid.x4, - text: @Composable (String) -> Unit = { PriceWithFlagDefaults.Text(label = it) }, -) { - PriceWithFlag( - modifier = modifier, - currencyCode = currencyCode.name, - amount = amount.formatted(), - flag = currencyCode.flagResId, - iconSize = iconSize, - text = text - ) -} - @Composable fun PriceWithFlag( modifier: Modifier = Modifier, diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt index 855af1f55..b4e0f8c2c 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/Feedback.kt @@ -32,10 +32,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.zIndex -import com.getcode.extensions.formattedRaw import com.getcode.model.RandomId import com.getcode.model.chat.Sender -import com.getcode.model.sum +import com.getcode.utils.FormatUtils import com.getcode.theme.CodeTheme import com.getcode.ui.components.R import com.getcode.ui.components.chat.UserAvatar @@ -117,7 +116,8 @@ private fun TipCounter( } ) - val totalTips = tips.map { it.amount }.sum().formattedRaw() + // TODO(chat-v2): reimplement tip formatting with OCP Fiat + val totalTips = FormatUtils.formatWholeRoundDown(tips.sumOf { it.amountInQuarks }.toDouble()) Row( modifier = modifier diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessagePayment.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessagePayment.kt index ef3260511..8ba023d20 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessagePayment.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/messagecontents/MessagePayment.kt @@ -7,9 +7,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -18,9 +15,9 @@ import androidx.compose.ui.unit.dp import com.getcode.model.chat.MessageContent import com.getcode.model.chat.MessageStatus import com.getcode.model.chat.Verb -import com.getcode.model.orOneToOne -import com.getcode.network.exchange.LocalExchange import com.getcode.theme.CodeTheme +import com.getcode.utils.FormatUtils +import com.getcode.utils.flagResId import com.getcode.ui.components.PriceWithFlag import com.getcode.ui.components.chat.utils.localizedText import kotlin.time.Instant @@ -42,15 +39,8 @@ internal fun MessagePayment( verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), horizontalAlignment = Alignment.CenterHorizontally, ) { - val exchange = LocalExchange.current - val rate by remember(contents.amount.currencyCode) { - derivedStateOf { - exchange.rateFor(contents.amount.currencyCode).orOneToOne() - } - } - val amount by remember(rate) { - derivedStateOf { contents.amount.amountUsing(rate) } - } + // TODO(chat-v2): reimplement payment formatting with OCP Fiat + val formattedAmount = FormatUtils.formatCurrency(contents.amount, contents.currencyCode) Column( modifier = Modifier @@ -61,8 +51,9 @@ internal fun MessagePayment( ) { if (contents.verb == Verb.Returned) { PriceWithFlag( - currencyCode = amount.rate.currency, - amount = amount, + currencyCode = contents.currencyCode.name, + amount = formattedAmount, + flag = contents.currencyCode.flagResId, text = { price -> Text( text = price, @@ -81,8 +72,9 @@ internal fun MessagePayment( style = CodeTheme.typography.textMedium.copy(fontWeight = FontWeight.W500) ) PriceWithFlag( - currencyCode = amount.rate.currency, - amount = amount, + currencyCode = contents.currencyCode.name, + amount = formattedAmount, + flag = contents.currencyCode.flagResId, text = { price -> Text( text = price, diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/utils/ChatItem.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/utils/ChatItem.kt index 567f9a605..0236eee9e 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/utils/ChatItem.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/utils/ChatItem.kt @@ -3,7 +3,6 @@ package com.getcode.ui.components.chat.utils import android.os.Parcelable import androidx.compose.runtime.Stable import com.getcode.model.ID -import com.getcode.model.KinAmount import com.getcode.model.chat.ChatMessage import com.getcode.model.chat.Deleter import com.getcode.model.chat.MessageContent @@ -30,8 +29,9 @@ data class ReplyMessageAnchor( val deletedBy: Deleter? = null, ) +// TODO(chat-v2): MessageTip will use OCP Fiat for amount @Parcelize -data class MessageTip(val amount: KinAmount, val tipper: Sender): Parcelable +data class MessageTip(val amountInQuarks: Long, val tipper: Sender): Parcelable @Parcelize data class MessageReaction(val messageId: ID, val sender: Sender, val emoji: String, val sentAt: Long): Parcelable diff --git a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/utils/LocalizedText.kt b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/utils/LocalizedText.kt index 9d25f6d7b..33b87bdba 100644 --- a/ui/components/src/main/kotlin/com/getcode/ui/components/chat/utils/LocalizedText.kt +++ b/ui/components/src/main/kotlin/com/getcode/ui/components/chat/utils/LocalizedText.kt @@ -5,17 +5,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext import com.getcode.ui.components.R -import com.getcode.model.Currency -import com.getcode.model.GenericAmount import com.getcode.model.chat.MessageContent import com.getcode.model.chat.Title import com.getcode.model.chat.Verb -import com.getcode.utils.Kin -import com.getcode.extensions.formatted import com.getcode.util.resources.ResourceHelper import com.getcode.util.resources.ResourceType import com.getcode.utils.FormatUtils -import com.getcode.utils.LocalCurrencyUtils import java.util.Locale val LocalLocalizeCurrencyFormatting = staticCompositionLocalOf { true } @@ -27,33 +22,8 @@ fun MessageContent.localizedText( ): String { return when (val content = this) { is MessageContent.Exchange -> { - val amount = when (val kinAmount = content.amount) { - is GenericAmount.Exact -> { - val currency = if (localizeCurrency) { - currencyUtils.getCurrency(kinAmount.currencyCode.name) - } else { - null - } - - val amount = if (localizeCurrency) { - kinAmount.amount.fiat - } else { - kinAmount.amount.kin.toKinValueDouble() - } - - kinAmount.amount.formatted( - resources = resources, - amount = amount, - currency = currency ?: Currency.Kin - ) - } - - is GenericAmount.Partial -> { - FormatUtils.formatCurrency(kinAmount.fiat.amount, kinAmount.currencyCode).let { - "$it ${resources.getOfKinSuffix()}" - } - } - } + // TODO(chat-v2): Exchange message amounts will use com.getcode.opencode.model.financial.Fiat + val amount = FormatUtils.formatCurrency(content.amount, content.currencyCode) val localized = content.verb.localizedText(resources) @@ -134,31 +104,8 @@ val MessageContent.localizedText: String val context = LocalContext.current return when (val content = this) { is MessageContent.Exchange -> { - val amount = when (val kinAmount = content.amount) { - is GenericAmount.Exact -> { - val localizeCurrencyFormatting = LocalLocalizeCurrencyFormatting.current - val currency = if (localizeCurrencyFormatting) { - LocalCurrencyUtils.current?.getCurrency(kinAmount.currencyCode.name) - } else { - null - } - - val amount = if (localizeCurrencyFormatting) { - kinAmount.amount.fiat - } else { - kinAmount.amount.kin.toKinValueDouble() - } - - kinAmount.amount.formatted( - amount = amount, - currency = currency ?: Currency.Kin - ) - } - - is GenericAmount.Partial -> { - FormatUtils.formatCurrency(kinAmount.fiat.amount, kinAmount.currencyCode) - } - } + // TODO(chat-v2): reimplement Exchange amount formatting with OCP Fiat + val amount = FormatUtils.formatCurrency(content.amount, content.currencyCode) val localized = content.verb.localizedText From 084a3fc988f755f609bcfab7eb6d92dbbd05eab6 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 13:05:22 -0400 Subject: [PATCH 3/4] chore(crypto): remove dead legacy Timelock, AgoraMemo, and PDA code These Solana program instruction files in libs/crypto are superseded by services/opencode equivalents and have zero external consumers. --- .../kotlin/com/getcode/solana/AgoraMemo.kt | 195 ------------------ .../instructions/programs/MemoProgram_Memo.kt | 32 --- .../instructions/programs/TimelockProgram.kt | 32 --- .../TimelockProgram_BurnDustWithAuthority.kt | 75 ------- .../programs/TimelockProgram_CloseAccounts.kt | 60 ------ .../TimelockProgram_DeactivateLock.kt | 56 ----- .../programs/TimelockProgram_Initialize.kt | 68 ------ ...TimelockProgram_RevokeLockWithAuthority.kt | 61 ------ .../TimelockProgram_TransferWithAuthority.kt | 76 ------- .../programs/TimelockProgram_Withdraw.kt | 63 ------ .../solana/keys/ProgramDerivedAccount.kt | 81 -------- .../com/getcode/solana/AgoraMemoTest.kt | 116 ----------- .../programs/TimelockProgramTest.kt | 192 ----------------- 13 files changed, 1107 deletions(-) delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/AgoraMemo.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/MemoProgram_Memo.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_BurnDustWithAuthority.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_CloseAccounts.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_DeactivateLock.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_Initialize.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_RevokeLockWithAuthority.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_TransferWithAuthority.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_Withdraw.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/keys/ProgramDerivedAccount.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/AgoraMemoTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/TimelockProgramTest.kt diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/AgoraMemo.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/AgoraMemo.kt deleted file mode 100644 index 74b4471ed..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/AgoraMemo.kt +++ /dev/null @@ -1,195 +0,0 @@ -package com.getcode.solana - -import com.getcode.solana.AgoraMemo.Companion.maxMagicByteIndicatorSize -import com.getcode.utils.decodeBase64 -import com.getcode.utils.encodeBase64ToArray -import com.getcode.utils.byteArrayToInt -import com.getcode.utils.shl -import com.getcode.utils.subByteArray -import kotlin.experimental.and -import kotlin.experimental.or -import kotlin.experimental.xor - -class AgoraMemo( - val magicByte: MagicByte = MagicByte.default, - val version: Byte = 1, - val transferType: TransferType, - val appIndex: Int, - bytes: List = listOf(), -) { - val bytes: List = List(byteLength) { bytes.getOrNull(it) ?: 0 } - - /** - * A memo format understood by the Agora services. - * magicByteIndicator 2 bits | less than 4 - * version 3 bits | less than 8 - * typeId 5 bits | less than 32 - * appIdx 16 bits | less than 65,536 - * foreignKey 230 bits | Base64 Encoded String of [230 bits + (2 zeros padding)] - */ - - /** - * Fields below are packed from LSB to MSB order: - * magicByteIndicator 2 bits | less than 4 - * version 3 bits | less than 8 - * typeId 5 bits | less than 32 - * appIdx 16 bits | less than 65,536 - * foreignKey 230 bits | Often a SHA-224 of an InvoiceList but could be anything - */ - - fun encode(): ByteArray { - val result = ByteArray(totalByteCount) - - result[0] = magicByte.rawValue - result[0] = result[0] xor (version shl 2) - result[0] = result[0] xor ((transferType.rawValue and 0x7).toByte() shl 5) - - result[1] = ((transferType.rawValue and 0x1c) shr 2).toByte() - result[1] = result[1] xor ((appIndex and 0x3f).toByte() shl 2) - - result[2] = ((appIndex and 0x3fc0) shr 6).toByte() - - result[3] = ((appIndex and 0xc000) shr 14).toByte() - - // Encode foreign key - result[3] = result[3] xor ((bytes[0] and 0x3f) shl 2) - - // Insert the rest of the fk. since each loop references fk[n] and fk[n + 1], the upper bound is offset by 3 instead of 4. - for (i in 4 until 3 + bytes.size) { - // apply last 2-bits of current byte - // apply first 6-bits of next byte - result[i] = (bytes[i - 4].toInt() shr 6).toByte() and 0x3 - result[i] = result[i] xor ((bytes[i - 3].toInt() and 0x3f).toByte() shl 2) - } - - // If the foreign key is less than 29 bytes, the last 2 bits of the FK can be included in the memo - if (bytes.size < 29) { - result[bytes.size + 3] = (bytes[bytes.size - 1].toInt() shr 6).toByte() and 0x3 - } - - return result.encodeBase64ToArray() - } - - - companion object { - const val magicByteBitLength: Int = 2 - const val versionBitLength: Int = 3 - const val transferTypeBitLength: Int = 5 - const val appIndexBitLength: Int = 16 - const val foreignKeyBitLength: Int = 230 - - const val maxMagicByteIndicatorSize: Int = 1 shl magicByteBitLength - - const val byteLength: Int = foreignKeyBitLength / 8 - - const val magicByteMask: Int = 0x3 - const val versionMask: Int = 0x1C - const val transferTypeMask: Int = 0x3E0 - const val appIndexMask: Int = 0x3FFFC00 - - const val magicByteIndicatorBitOffset: Int = 0 - - const val totalLowerByteCount: Int = - (magicByteBitLength + versionBitLength + transferTypeBitLength + appIndexBitLength) / 8 - const val totalByteCount: Int = - (magicByteBitLength + versionBitLength + transferTypeBitLength + appIndexBitLength + foreignKeyBitLength) / 8 - - - fun newInstance(data: List): AgoraMemo { - val content = data.toByteArray().decodeBase64() - val header = content.subByteArray(0, 4).byteArrayToInt() - - val byte = (header and magicByteMask) shr 0 - val version = (header and versionMask) shr 2 - val transferTypeValue = (header and transferTypeMask) shr 5 - val appIndex = (header and appIndexMask) shr 10 - - val magicByte = MagicByte(byte.toByte()) - val transferType = TransferType.getByValue(transferTypeValue.toByte()) - - if (transferType == TransferType.unknown) { - throw IllegalArgumentException("Invalid Value") - } - - val bytes = ByteArray(byteLength) - - for (i in 0 until byteLength) { - bytes[i] = bytes[i] or (content[i + 3].toInt() shr 2).toByte() and 0x3F - bytes[i] = bytes[i] or ((content[i + 4] and 0x3) shl 6) - } - - return AgoraMemo( - magicByte = magicByte, - version = version.toByte(), - transferType = transferType, - appIndex = appIndex, - bytes = bytes.toList() - ) - - } - - } -} - -class MagicByte(val rawValue: Byte) { - init { - val isValid = rawValue in 1 until maxMagicByteIndicatorSize - //rawValue > 0 && rawValue < maxMagicByteIndicatorSize - if (!isValid) throw IllegalArgumentException("Invalid Value") - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MagicByte - - if (rawValue != other.rawValue) return false - - return true - } - - override fun hashCode(): Int { - return rawValue.toInt() - } - - companion object { - val default = MagicByte(rawValue = 1) - } -} - -enum class TransferType(val rawValue: Int) { - unknown(Byte.MAX_VALUE.toInt() and 0xFF), - - /// When none of the other types are appropriate for the use case. - none(0), - - /// Use when transferring Kin to a user for some performed action. - earn(1), - - /// Use when transferring Kin due to purchasing something. - spend(2), - - /// Use when transferring Kin where it does not constitute an `earn` or `spend`. - p2p(3); - - companion object { - fun getByValue(rawValue: Byte): TransferType { - return when (rawValue.toInt()) { - 0 -> none - 1 -> earn - 2 -> spend - 3 -> p2p - else -> unknown - } - } - } -} - -enum class Error { - invalidData, - invalidMagicByte, - invalidVersion, - invalidTransferType, - invalidAppIndex, -} diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/MemoProgram_Memo.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/MemoProgram_Memo.kt deleted file mode 100644 index d1e3c0405..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/MemoProgram_Memo.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.AgoraMemo -import com.getcode.solana.Instruction -import com.getcode.solana.TransferType - -class MemoProgram_Memo(data: List) : MemoProgram(data) { - fun instruction(): Instruction { - return Instruction( - program = address, - accounts = listOf(), - data = encode() - ) - } - - fun encode() = data - - companion object { - fun newInstance(transferType: TransferType, kreIndex: Int): MemoProgram_Memo { - return MemoProgram_Memo( - data = AgoraMemo(transferType = transferType, appIndex = kreIndex).encode().toList() - ) - } - - /*fun newInstance(instruction: Instruction): MemoProgram_Memo { - MemoProgram.parse(instruction = instruction, expectingAccounts = 0) - return MemoProgram_Memo( - data = instruction.data - ) - }*/ - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram.kt deleted file mode 100644 index 0728e7cb3..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.utils.DataSlice.toLong -import com.getcode.vendor.Base58 - -class TimelockProgram { - - enum class Command(val value: Long) { - initialize ("ED9B980D1F6DAFAF".toULong(16).toLong()), - activate ("52AA37976423CBC2".toULong(16).toLong()), - transferWithAuthority ("A5474581C0DE8044".toULong(16).toLong()), - revokeLockWithAuthority ("90C908ABF23AB5E5".toULong(16).toLong()), - deactivateLock ("0D8E1C71AC21702C".toULong(16).toLong()), - withdraw ("22A16D949C4612B7".toULong(16).toLong()), - closeAccounts ("01CAFA22E95EDEAB".toULong(16).toLong()), - burnDustWithAuthority ("2D4E7C0EDAFF2A27".toULong(16).toLong()), - } - companion object: com.getcode.solana.instructions.CommandType() { - override val address = com.getcode.solana.keys.PublicKey( - Base58.decode("time2Z2SCnn3qYg3ULKVtdkh8YmZ5jFdKicnA1W2YnJ").toList() - ) - val legacyAddress = com.getcode.solana.keys.PublicKey( - Base58.decode("timeDBoQGL52du9K7EtrhkJSqpiFapE9dHrmDVkuZx6").toList() - ) - - override val commandByteLength: Int get() = Long.SIZE_BYTES - - override fun commandLookup(bytes: ByteArray): Command? { - return Command.entries.firstOrNull { it.value == bytes.toLong() } - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_BurnDustWithAuthority.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_BurnDustWithAuthority.kt deleted file mode 100644 index 9ac9a22c9..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_BurnDustWithAuthority.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.model.Kin -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction -import com.getcode.solana.instructions.programs.TimelockProgram.Command -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.toByteArray -import com.getcode.utils.byteArrayToLong -import com.getcode.utils.longToByteArray - -class TimelockProgram_BurnDustWithAuthority( - val timelock: com.getcode.solana.keys.PublicKey, - val vault: com.getcode.solana.keys.PublicKey, - val vaultOwner: com.getcode.solana.keys.PublicKey, - val timeAuthority: com.getcode.solana.keys.PublicKey, - val mint: com.getcode.solana.keys.PublicKey, - val payer: com.getcode.solana.keys.PublicKey, - val bump: Byte, - val maxAmount: Kin, - val legacy: Boolean = false, -) : com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - return Instruction( - program = if (legacy) TimelockProgram.legacyAddress else TimelockProgram.address, - accounts = listOf( - AccountMeta.writable(publicKey = timelock), - AccountMeta.writable(publicKey = vault), - - AccountMeta.readonly(publicKey = vaultOwner, signer = true), - AccountMeta.readonly(publicKey = timeAuthority, signer = true), - - AccountMeta.writable(publicKey = mint), - AccountMeta.writable(publicKey = payer, signer = true), - - AccountMeta.readonly(publicKey = TokenProgram.address), - AccountMeta.readonly(publicKey = SystemProgram.address), - ), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.addAll(Command.burnDustWithAuthority.value.toByteArray().toList()) - data.add(bump) - data.addAll(maxAmount.quarks.longToByteArray().toList()) - return data - } - - companion object { - fun newInstance(instruction: Instruction): TimelockProgram_BurnDustWithAuthority { - val data = TimelockProgram.parse( - command = Command.burnDustWithAuthority, - instruction = instruction, - expectingAccounts = 8 - ) - - - val bump = data.remaining.consume(1) - val maxAmount = bump.remaining.consume(8).consumed.toByteArray().byteArrayToLong() - - return TimelockProgram_BurnDustWithAuthority( - timelock = instruction.accounts[0].publicKey, - vault = instruction.accounts[1].publicKey, - vaultOwner = instruction.accounts[2].publicKey, - timeAuthority = instruction.accounts[3].publicKey, - mint = instruction.accounts[4].publicKey, - payer = instruction.accounts[5].publicKey, - bump = bump.consumed.first(), - maxAmount = Kin.fromQuarks(maxAmount) - ) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_CloseAccounts.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_CloseAccounts.kt deleted file mode 100644 index 66e9d2c2d..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_CloseAccounts.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction -import com.getcode.solana.instructions.programs.TimelockProgram.Command.closeAccounts -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.toByteArray - -// Reference: https://github.com/code-wallet/code-server/blob/privacy-v3/pkg/solana/timelock/instruction_closeaccounts.go -class TimelockProgram_CloseAccounts( - val timelock: com.getcode.solana.keys.PublicKey, - val vault: com.getcode.solana.keys.PublicKey, - val closeAuthority: com.getcode.solana.keys.PublicKey, - val payer: com.getcode.solana.keys.PublicKey, - val bump: Byte, - val legacy: Boolean = false, -) : com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - return Instruction( - program = if (legacy) TimelockProgram.legacyAddress else TimelockProgram.address, - accounts = listOf( - AccountMeta.writable(publicKey = timelock), - AccountMeta.writable(publicKey = vault), - AccountMeta.readonly(publicKey = closeAuthority, signer = true), - AccountMeta.writable(publicKey = payer, signer = true), - AccountMeta.readonly(publicKey = TokenProgram.address), - AccountMeta.readonly(publicKey = SystemProgram.address), - ), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.addAll(closeAccounts.value.toByteArray().toList()) - data.add(bump) - return data - } - - companion object { - fun newInstance(instruction: Instruction): TimelockProgram_CloseAccounts { - val data = TimelockProgram.parse( - command = closeAccounts, - instruction = instruction, - expectingAccounts = 6 - ) - - - val bump = data.remaining.consume(1) - - return TimelockProgram_CloseAccounts( - timelock = instruction.accounts[0].publicKey, - vault = instruction.accounts[1].publicKey, - closeAuthority = instruction.accounts[2].publicKey, - payer = instruction.accounts[3].publicKey, - bump = bump.consumed.first() - ) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_DeactivateLock.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_DeactivateLock.kt deleted file mode 100644 index d29c50869..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_DeactivateLock.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction -import com.getcode.solana.instructions.programs.TimelockProgram.Command -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.toByteArray - -// Reference: https://github.com/code-wallet/code-server/blob/privacy-v3/pkg/solana/timelock/instruction_deactivate.go -class TimelockProgram_DeactivateLock( - val timelock: com.getcode.solana.keys.PublicKey, - val vaultOwner: com.getcode.solana.keys.PublicKey, - val payer: com.getcode.solana.keys.PublicKey, - val bump: Byte, - val legacy: Boolean = false, -) : com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - return Instruction( - program = if (legacy) TimelockProgram.legacyAddress else TimelockProgram.address, - accounts = listOf( - AccountMeta.writable(publicKey = timelock), - AccountMeta.readonly(publicKey = vaultOwner, signer = true), - AccountMeta.writable(publicKey = payer, signer = true), - ), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.addAll(Command.deactivateLock.value.toByteArray().toList()) - data.add(bump) - - return data - } - - companion object { - fun newInstance(instruction: Instruction): TimelockProgram_DeactivateLock { - val data = TimelockProgram.parse( - command = Command.deactivateLock, - instruction = instruction, - expectingAccounts = 3 - ) - - - val bump = data.remaining.consume(1) - - return TimelockProgram_DeactivateLock( - timelock = instruction.accounts[0].publicKey, - vaultOwner = instruction.accounts[1].publicKey, - payer = instruction.accounts[2].publicKey, - bump = bump.consumed.first() - ) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_Initialize.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_Initialize.kt deleted file mode 100644 index 8361d9db6..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_Initialize.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction -import com.getcode.solana.instructions.programs.TimelockProgram.Command -import com.getcode.utils.toByteArray -import com.getcode.utils.byteArrayToLong -import com.getcode.utils.longToByteArray - -// Reference: https://github.com/code-wallet/code-server/blob/master/pkg/solana/timelock/instruction_initialize.go -class TimelockProgram_Initialize( - val nonce: com.getcode.solana.keys.PublicKey, - val timelock: com.getcode.solana.keys.PublicKey, - val vault: com.getcode.solana.keys.PublicKey, - val vaultOwner: com.getcode.solana.keys.PublicKey, - val mint: com.getcode.solana.keys.PublicKey, - val timeAuthority: com.getcode.solana.keys.PublicKey, - val payer: com.getcode.solana.keys.PublicKey, - val lockout: Long -) : com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - return Instruction( - program = TimelockProgram.address, - accounts = listOf( - AccountMeta.readonly(publicKey = nonce), - AccountMeta.writable(publicKey = timelock), - AccountMeta.writable(publicKey = vault), - AccountMeta.readonly(publicKey = vaultOwner), - AccountMeta.readonly(publicKey = mint), - AccountMeta.readonly(publicKey = timeAuthority, signer = true), - AccountMeta.writable(publicKey = payer, signer = true), - AccountMeta.readonly(publicKey = TokenProgram.address), - AccountMeta.readonly(publicKey = SystemProgram.address), - AccountMeta.readonly(publicKey = SysVar.rent.address()), - ), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.addAll(Command.initialize.value.toByteArray().toList()) - data.addAll(lockout.longToByteArray().toList()) - - return data - } - - companion object { - fun newInstance(instruction: Instruction): TimelockProgram_Initialize { - val data = TimelockProgram.parse( - command = Command.initialize, - instruction = instruction, - expectingAccounts = 10 - ) - - return TimelockProgram_Initialize( - nonce = instruction.accounts[0].publicKey, - timelock = instruction.accounts[1].publicKey, - vault = instruction.accounts[2].publicKey, - vaultOwner = instruction.accounts[3].publicKey, - mint = instruction.accounts[4].publicKey, - timeAuthority = instruction.accounts[5].publicKey, - payer = instruction.accounts[6].publicKey, - lockout = data.remaining.toByteArray().byteArrayToLong() - ) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_RevokeLockWithAuthority.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_RevokeLockWithAuthority.kt deleted file mode 100644 index 80a473465..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_RevokeLockWithAuthority.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction -import com.getcode.solana.instructions.programs.TimelockProgram.Command -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.toByteArray - -// Reference: https://github.com/code-wallet/code-server/blob/privacy-v3/pkg/solana/timelock/instruction_revokelockwithauthority.go -class TimelockProgram_RevokeLockWithAuthority( - val timelock: com.getcode.solana.keys.PublicKey, - val vault: com.getcode.solana.keys.PublicKey, - val closeAuthority: com.getcode.solana.keys.PublicKey, - val payer: com.getcode.solana.keys.PublicKey, - val bump: Byte, - val legacy: Boolean = false, -) : com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - return Instruction( - program = if (legacy) TimelockProgram.legacyAddress else TimelockProgram.address, - accounts = listOf( - AccountMeta.writable(publicKey = timelock), - AccountMeta.writable(publicKey = vault), - AccountMeta.readonly(publicKey = closeAuthority, signer = true), - AccountMeta.writable(publicKey = payer, signer = true), - AccountMeta.readonly(publicKey = TokenProgram.address), - AccountMeta.readonly(publicKey = SystemProgram.address), - ), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.addAll(Command.revokeLockWithAuthority.value.toByteArray().toList()) - data.add(bump) - - return data - } - - companion object { - fun newInstance(instruction: Instruction): TimelockProgram_RevokeLockWithAuthority { - val data = TimelockProgram.parse( - command = Command.revokeLockWithAuthority, - instruction = instruction, - expectingAccounts = 6 - ) - - - val bump = data.remaining.consume(1) - - return TimelockProgram_RevokeLockWithAuthority( - timelock = instruction.accounts[0].publicKey, - vault = instruction.accounts[1].publicKey, - closeAuthority = instruction.accounts[2].publicKey, - payer = instruction.accounts[3].publicKey, - bump = bump.consumed.first() - ) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_TransferWithAuthority.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_TransferWithAuthority.kt deleted file mode 100644 index 16f487897..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_TransferWithAuthority.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.model.Kin -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction -import com.getcode.solana.instructions.programs.TimelockProgram.Command -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.toByteArray -import com.getcode.utils.byteArrayToLong -import com.getcode.utils.longToByteArray - -// Reference: https://github.com/code-wallet/code-server/blob/master/pkg/solana/timelock/instruction_transferwithauthority.go -class TimelockProgram_TransferWithAuthority( - val timelock: com.getcode.solana.keys.PublicKey, - val vault: com.getcode.solana.keys.PublicKey, - val vaultOwner: com.getcode.solana.keys.PublicKey, - val timeAuthority: com.getcode.solana.keys.PublicKey, - val destination: com.getcode.solana.keys.PublicKey, - val payer: com.getcode.solana.keys.PublicKey, - val bump: Byte, - val quarks: Long -) : com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - return Instruction( - program = TimelockProgram.address, - accounts = listOf( - AccountMeta.readonly(publicKey = timelock), - AccountMeta.writable(publicKey = vault), - AccountMeta.readonly(publicKey = vaultOwner, signer = true), - AccountMeta.readonly(publicKey = timeAuthority, signer = true), - AccountMeta.writable(publicKey = destination), - AccountMeta.writable(publicKey = payer, signer = true), - AccountMeta.readonly(publicKey = TokenProgram.address), - AccountMeta.readonly(publicKey = SystemProgram.address), - ), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.addAll( - Command.transferWithAuthority.value.toByteArray().toList() - ) - data.add(bump) - data.addAll(quarks.longToByteArray().toList()) - - return data - } - - - companion object { - fun newInstance(instruction: Instruction): TimelockProgram_TransferWithAuthority { - val data = TimelockProgram.parse( - command = Command.transferWithAuthority, - instruction = instruction, - expectingAccounts = 8 - ) - - val bump = data.remaining.consume(1) - - val quarks = bump.remaining.consume(8).consumed.toByteArray().byteArrayToLong() - - return TimelockProgram_TransferWithAuthority( - timelock = instruction.accounts[0].publicKey, - vault = instruction.accounts[1].publicKey, - vaultOwner = instruction.accounts[2].publicKey, - timeAuthority = instruction.accounts[3].publicKey, - destination = instruction.accounts[4].publicKey, - payer = instruction.accounts[5].publicKey, - bump = bump.consumed.first(), - quarks = quarks - ) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_Withdraw.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_Withdraw.kt deleted file mode 100644 index f559cb4f2..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TimelockProgram_Withdraw.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction -import com.getcode.solana.instructions.programs.TimelockProgram.Command -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.toByteArray - -// Reference: https://github.com/code-wallet/code-server/blob/privacy-v3/pkg/solana/timelock/instruction_withdraw.go -class TimelockProgram_Withdraw( - val timelock: com.getcode.solana.keys.PublicKey, - val vault: com.getcode.solana.keys.PublicKey, - val vaultOwner: com.getcode.solana.keys.PublicKey, - val destination: com.getcode.solana.keys.PublicKey, - val payer: com.getcode.solana.keys.PublicKey, - val bump: Byte, - val legacy: Boolean = false, -) : com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - return Instruction( - program = if (legacy) TimelockProgram.legacyAddress else TimelockProgram.address, - accounts = listOf( - AccountMeta.readonly(publicKey = timelock), - AccountMeta.writable(publicKey = vault), - AccountMeta.readonly(publicKey = vaultOwner, signer = true), - AccountMeta.writable(publicKey = destination), - AccountMeta.writable(publicKey = payer, signer = true), - AccountMeta.readonly(publicKey = TokenProgram.address), - AccountMeta.readonly(publicKey = SystemProgram.address), - ), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.addAll(Command.withdraw.value.toByteArray().toList()) - data.add(bump) - - return data - } - - companion object { - fun newInstance(instruction: Instruction): TimelockProgram_Withdraw { - val data = TimelockProgram.parse( - command = Command.withdraw, - instruction = instruction, - expectingAccounts = 7 - ) - - val bump = data.remaining.consume(1) - - return TimelockProgram_Withdraw( - timelock = instruction.accounts[0].publicKey, - vault = instruction.accounts[1].publicKey, - vaultOwner = instruction.accounts[2].publicKey, - destination = instruction.accounts[3].publicKey, - payer = instruction.accounts[4].publicKey, - bump = bump.consumed.first() - ) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/keys/ProgramDerivedAccount.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/keys/ProgramDerivedAccount.kt deleted file mode 100644 index 04c6b7fab..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/keys/ProgramDerivedAccount.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.getcode.solana.keys - -import com.getcode.crypt.Sha256Hash -import com.getcode.model.Kin -import com.getcode.utils.toUTF8Bytes - -data class ProgramDerivedAccount(val publicKey: PublicKey, val bump: Int) - -class TimelockDerivedAccounts( - val owner: PublicKey, - val state: ProgramDerivedAccount, - val vault: ProgramDerivedAccount -) { - companion object { - const val lockoutInDays: Long = 21 - const val dataVersion: Long = 3 - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as TimelockDerivedAccounts - - if (owner != other.owner) return false - if (state != other.state) return false - if (vault != other.vault) return false - - return true - } - - override fun hashCode(): Int { - var result = owner.hashCode() - result = 31 * result + state.hashCode() - result = 31 * result + vault.hashCode() - return result - } -} - -class SplitterCommitmentAccounts( - val treasury: PublicKey, - val destination: PublicKey, - val recentRoot: Hash, - val transcript: Hash, - - val state: ProgramDerivedAccount, - val vault: ProgramDerivedAccount, -) { - companion object -} - -data class SplitterTranscript( - val intentId: PublicKey, - val actionId: Int, val - amount: Kin, - val source: PublicKey, - val destination: PublicKey -) { - val description = - "receipt[${intentId.base58()}, $actionId]: " + - "transfer ${amount.quarks} quarks " + - "from ${source.base58()} to ${destination.base58()}" - - val transcriptHash: Hash = - Hash(Sha256Hash.hash(description.toUTF8Bytes()).toList()) -} - -data class AssociatedTokenAccount( - val owner: PublicKey, - val ata: ProgramDerivedAccount, -) { - companion object -} - -data class PreSwapStateAccount( - val owner: PublicKey, - val state: ProgramDerivedAccount, -) { - companion object -} - diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/AgoraMemoTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/AgoraMemoTest.kt deleted file mode 100644 index 03b5fbb47..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/AgoraMemoTest.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.getcode.solana - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -class AgoraMemoTest { - - // Note: encode/decode roundtrip tests require Android Base64 and cannot run as JVM unit tests. - // Testing construction, validation, and field access instead. - - @Test - fun constructionWithDefaults() { - val memo = AgoraMemo(transferType = TransferType.none, appIndex = 0) - assertEquals(MagicByte.default, memo.magicByte) - assertEquals(1.toByte(), memo.version) - assertEquals(TransferType.none, memo.transferType) - assertEquals(0, memo.appIndex) - } - - @Test - fun constructionWithAllTransferTypes() { - for (type in listOf(TransferType.none, TransferType.earn, TransferType.spend, TransferType.p2p)) { - val memo = AgoraMemo(transferType = type, appIndex = 10) - assertEquals(type, memo.transferType) - } - } - - @Test - fun foreignKeyPaddedToByteLength() { - val shortKey = listOf(1, 2, 3) - val memo = AgoraMemo(transferType = TransferType.earn, appIndex = 0, bytes = shortKey) - assertEquals(AgoraMemo.byteLength, memo.bytes.size) - assertEquals(1.toByte(), memo.bytes[0]) - assertEquals(2.toByte(), memo.bytes[1]) - assertEquals(3.toByte(), memo.bytes[2]) - // Remaining bytes should be zero-padded - for (i in 3 until AgoraMemo.byteLength) { - assertEquals(0.toByte(), memo.bytes[i]) - } - } - - @Test - fun foreignKeyFullLength() { - val fullKey = List(AgoraMemo.byteLength) { (it + 1).toByte() } - val memo = AgoraMemo(transferType = TransferType.p2p, appIndex = 42, bytes = fullKey) - assertEquals(fullKey, memo.bytes) - } - - @Test - fun magicByteValidValues() { - MagicByte(1) - MagicByte(2) - MagicByte(3) - } - - @Test - fun magicByteInvalidZeroThrows() { - assertFailsWith { - MagicByte(0) - } - } - - @Test - fun magicByteInvalidFourThrows() { - assertFailsWith { - MagicByte(4) - } - } - - @Test - fun magicByteEquality() { - assertEquals(MagicByte(1), MagicByte(1)) - } - - @Test - fun magicByteHashCode() { - assertEquals(MagicByte(1).hashCode(), MagicByte(1).hashCode()) - } - - @Test - fun transferTypeGetByValueNone() { - assertEquals(TransferType.none, TransferType.getByValue(0)) - } - - @Test - fun transferTypeGetByValueEarn() { - assertEquals(TransferType.earn, TransferType.getByValue(1)) - } - - @Test - fun transferTypeGetByValueSpend() { - assertEquals(TransferType.spend, TransferType.getByValue(2)) - } - - @Test - fun transferTypeGetByValueP2p() { - assertEquals(TransferType.p2p, TransferType.getByValue(3)) - } - - @Test - fun transferTypeGetByValueUnknown() { - assertEquals(TransferType.unknown, TransferType.getByValue(99)) - } - - @Test - fun companionConstants() { - assertEquals(2, AgoraMemo.magicByteBitLength) - assertEquals(3, AgoraMemo.versionBitLength) - assertEquals(5, AgoraMemo.transferTypeBitLength) - assertEquals(16, AgoraMemo.appIndexBitLength) - assertEquals(230, AgoraMemo.foreignKeyBitLength) - assertEquals(28, AgoraMemo.byteLength) // 230 / 8 - assertEquals(32, AgoraMemo.totalByteCount) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/TimelockProgramTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/TimelockProgramTest.kt deleted file mode 100644 index 91dacd228..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/TimelockProgramTest.kt +++ /dev/null @@ -1,192 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.model.Kin -import com.getcode.solana.keys.PublicKey -import kotlin.test.Test -import kotlin.test.assertEquals - -class TimelockProgramTest { - - private fun key(seed: Byte) = PublicKey(ByteArray(32) { seed }.toList()) - - // --- Initialize --- - - @Test - fun initializeRoundtrip() { - val original = TimelockProgram_Initialize( - nonce = key(1), timelock = key(2), vault = key(3), vaultOwner = key(4), - mint = key(5), timeAuthority = key(6), payer = key(7), lockout = 86400L, - ) - val decoded = TimelockProgram_Initialize.newInstance(original.instruction()) - assertEquals(86400L, decoded.lockout) - assertEquals(original.nonce, decoded.nonce) - assertEquals(original.timelock, decoded.timelock) - assertEquals(original.vault, decoded.vault) - } - - @Test - fun initializeLockoutZero() { - val original = TimelockProgram_Initialize( - nonce = key(1), timelock = key(2), vault = key(3), vaultOwner = key(4), - mint = key(5), timeAuthority = key(6), payer = key(7), lockout = 0L, - ) - val decoded = TimelockProgram_Initialize.newInstance(original.instruction()) - assertEquals(0L, decoded.lockout) - } - - @Test - fun initializeEncodeLength() { - val inst = TimelockProgram_Initialize( - key(1), key(2), key(3), key(4), key(5), key(6), key(7), lockout = 1L, - ) - // 8 bytes command + 8 bytes lockout - assertEquals(16, inst.encode().size) - } - - // --- TransferWithAuthority --- - - @Test - fun transferWithAuthorityRoundtrip() { - val original = TimelockProgram_TransferWithAuthority( - timelock = key(1), vault = key(2), vaultOwner = key(3), timeAuthority = key(4), - destination = key(5), payer = key(6), bump = 42, quarks = 1_000_000L, - ) - val decoded = TimelockProgram_TransferWithAuthority.newInstance(original.instruction()) - assertEquals(42.toByte(), decoded.bump) - assertEquals(1_000_000L, decoded.quarks) - assertEquals(original.timelock, decoded.timelock) - assertEquals(original.destination, decoded.destination) - } - - @Test - fun transferWithAuthorityZeroQuarks() { - val original = TimelockProgram_TransferWithAuthority( - timelock = key(1), vault = key(2), vaultOwner = key(3), timeAuthority = key(4), - destination = key(5), payer = key(6), bump = 0, quarks = 0L, - ) - val decoded = TimelockProgram_TransferWithAuthority.newInstance(original.instruction()) - assertEquals(0.toByte(), decoded.bump) - assertEquals(0L, decoded.quarks) - } - - @Test - fun transferWithAuthorityEncodeLength() { - val inst = TimelockProgram_TransferWithAuthority( - key(1), key(2), key(3), key(4), key(5), key(6), bump = 1, quarks = 1L, - ) - // 8 bytes command + 1 byte bump + 8 bytes quarks - assertEquals(17, inst.encode().size) - } - - // --- Withdraw --- - - @Test - fun withdrawRoundtrip() { - val original = TimelockProgram_Withdraw( - timelock = key(1), vault = key(2), vaultOwner = key(3), - destination = key(4), payer = key(5), bump = 7, - ) - val decoded = TimelockProgram_Withdraw.newInstance(original.instruction()) - assertEquals(7.toByte(), decoded.bump) - assertEquals(original.vault, decoded.vault) - assertEquals(original.destination, decoded.destination) - } - - @Test - fun withdrawEncodeLength() { - val inst = TimelockProgram_Withdraw(key(1), key(2), key(3), key(4), key(5), bump = 1) - // 8 bytes command + 1 byte bump - assertEquals(9, inst.encode().size) - } - - // --- CloseAccounts --- - - @Test - fun closeAccountsRoundtrip() { - val original = TimelockProgram_CloseAccounts( - timelock = key(1), vault = key(2), closeAuthority = key(3), - payer = key(4), bump = 3, - ) - val decoded = TimelockProgram_CloseAccounts.newInstance(original.instruction()) - assertEquals(3.toByte(), decoded.bump) - assertEquals(original.closeAuthority, decoded.closeAuthority) - } - - @Test - fun closeAccountsEncodeLength() { - val inst = TimelockProgram_CloseAccounts(key(1), key(2), key(3), key(4), bump = 1) - assertEquals(9, inst.encode().size) - } - - // --- DeactivateLock --- - - @Test - fun deactivateLockRoundtrip() { - val original = TimelockProgram_DeactivateLock( - timelock = key(1), vaultOwner = key(2), payer = key(3), bump = 99.toByte(), - ) - val decoded = TimelockProgram_DeactivateLock.newInstance(original.instruction()) - assertEquals(99.toByte(), decoded.bump) - assertEquals(original.timelock, decoded.timelock) - } - - @Test - fun deactivateLockEncodeLength() { - val inst = TimelockProgram_DeactivateLock(key(1), key(2), key(3), bump = 1) - assertEquals(9, inst.encode().size) - } - - // --- RevokeLockWithAuthority --- - - @Test - fun revokeLockRoundtrip() { - val original = TimelockProgram_RevokeLockWithAuthority( - timelock = key(1), vault = key(2), closeAuthority = key(3), - payer = key(4), bump = 5, - ) - val decoded = TimelockProgram_RevokeLockWithAuthority.newInstance(original.instruction()) - assertEquals(5.toByte(), decoded.bump) - assertEquals(original.vault, decoded.vault) - } - - @Test - fun revokeLockEncodeLength() { - val inst = TimelockProgram_RevokeLockWithAuthority(key(1), key(2), key(3), key(4), bump = 1) - assertEquals(9, inst.encode().size) - } - - // --- BurnDustWithAuthority --- - - @Test - fun burnDustRoundtrip() { - val maxAmount = Kin.fromQuarks(500_000L) - val original = TimelockProgram_BurnDustWithAuthority( - timelock = key(1), vault = key(2), vaultOwner = key(3), timeAuthority = key(4), - mint = key(5), payer = key(6), bump = 12, maxAmount = maxAmount, - ) - val decoded = TimelockProgram_BurnDustWithAuthority.newInstance(original.instruction()) - assertEquals(12.toByte(), decoded.bump) - assertEquals(maxAmount.quarks, decoded.maxAmount.quarks) - } - - @Test - fun burnDustZeroAmount() { - val original = TimelockProgram_BurnDustWithAuthority( - timelock = key(1), vault = key(2), vaultOwner = key(3), timeAuthority = key(4), - mint = key(5), payer = key(6), bump = 0, maxAmount = Kin.fromQuarks(0), - ) - val decoded = TimelockProgram_BurnDustWithAuthority.newInstance(original.instruction()) - assertEquals(0.toByte(), decoded.bump) - assertEquals(0L, decoded.maxAmount.quarks) - } - - @Test - fun burnDustEncodeLength() { - val inst = TimelockProgram_BurnDustWithAuthority( - key(1), key(2), key(3), key(4), key(5), key(6), - bump = 1, maxAmount = Kin.fromQuarks(1), - ) - // 8 bytes command + 1 byte bump + 8 bytes maxAmount - assertEquals(17, inst.encode().size) - } -} From 8b543982beed02ee331d8e28fae83bbabac0fa9c Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 4 Jun 2026 13:13:10 -0400 Subject: [PATCH 4/4] chore(crypto): remove dead legacy Solana instruction/transaction code Delete 37 files (25 main + 12 tests) from libs/crypto/solana that are fully superseded by services/opencode equivalents with zero external consumers. Retains only the RPC layer (Calls, Requests, RpcConfig) and SolanaModule used by PhantomWalletController. --- .../kotlin/com/getcode/solana/Instruction.kt | 137 --------- .../main/kotlin/com/getcode/solana/Message.kt | 118 -------- .../com/getcode/solana/MessageHeader.kt | 52 ---- .../kotlin/com/getcode/solana/ShortVec.kt | 91 ------ .../com/getcode/solana/SolanaTransaction.kt | 223 --------------- .../instructions/AssociatedTokenAccounts.kt | 33 --- .../solana/instructions/InstructionType.kt | 60 ---- .../solana/instructions/SplTransfer.kt | 61 ----- .../programs/AssociatedTokenProgram.kt | 11 - .../AssociatedTokenProgram_CreateAccount.kt | 63 ----- .../programs/ComputeBudgetProgram.kt | 26 -- .../ComputeBudgetProgram_RequestUnits.kt | 48 ---- ...omputeBudgetProgram_SetComputeUnitLimit.kt | 43 --- ...omputeBudgetProgram_SetComputeUnitPrice.kt | 40 --- .../instructions/programs/MemoProgram.kt | 30 -- .../programs/SwapValidatorProgram.kt | 23 -- .../programs/SwapValidatorProgram_PostSwap.kt | 72 ----- .../programs/SwapValidatorProgram_PreSwap.kt | 62 ----- .../solana/instructions/programs/SysVar.kt | 19 -- .../instructions/programs/SystemProgram.kt | 27 -- .../programs/SystemProgram_AdvanceNonce.kt | 38 --- .../instructions/programs/TokenProgram.kt | 11 - .../getcode/solana/transactions/Message.kt | 157 ----------- .../kotlin/com/getcode/utils/DataSlice.kt | 59 ---- .../main/kotlin/com/getcode/utils/Differ.kt | 41 --- .../com/getcode/solana/InstructionTest.kt | 167 ----------- .../com/getcode/solana/MessageHeaderTest.kt | 88 ------ .../kotlin/com/getcode/solana/MessageTest.kt | 182 ------------ .../com/getcode/solana/ProgramAddressTest.kt | 57 ---- .../kotlin/com/getcode/solana/ShortVecTest.kt | 86 ------ .../getcode/solana/SolanaTransactionTest.kt | 259 ------------------ .../programs/AssociatedTokenProgramTest.kt | 34 --- .../programs/ComputeBudgetProgramTest.kt | 99 ------- .../programs/SwapValidatorProgramTest.kt | 94 ------- .../programs/SystemProgramTest.kt | 36 --- .../getcode/solana/keys/AccountMetaTest.kt | 188 ------------- .../kotlin/com/getcode/utils/DataSliceTest.kt | 153 ----------- 37 files changed, 2988 deletions(-) delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/Instruction.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/Message.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/MessageHeader.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/ShortVec.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/SolanaTransaction.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/AssociatedTokenAccounts.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/InstructionType.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/SplTransfer.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgram.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgram_CreateAccount.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_RequestUnits.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_SetComputeUnitLimit.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_SetComputeUnitPrice.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/MemoProgram.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram_PostSwap.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram_PreSwap.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SysVar.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SystemProgram.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SystemProgram_AdvanceNonce.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TokenProgram.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/solana/transactions/Message.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/utils/DataSlice.kt delete mode 100644 libs/crypto/solana/src/main/kotlin/com/getcode/utils/Differ.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/InstructionTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/MessageHeaderTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/MessageTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/ProgramAddressTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/ShortVecTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/SolanaTransactionTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgramTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgramTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgramTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/SystemProgramTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/solana/keys/AccountMetaTest.kt delete mode 100644 libs/crypto/solana/src/test/kotlin/com/getcode/utils/DataSliceTest.kt diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/Instruction.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/Instruction.kt deleted file mode 100644 index 13e3539d4..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/Instruction.kt +++ /dev/null @@ -1,137 +0,0 @@ -package com.getcode.solana - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.base58 -import com.getcode.solana.keys.description -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.DataSlice.prefix -import com.getcode.utils.hexEncodedString - -data class Instruction( - val program: PublicKey, - val accounts: List, - val data: List, -) { - fun compile(messageAccounts: List): CompiledInstruction { - val programIndex = messageAccounts.indexOfFirst { it == program } - val accountIndexes = accounts.map { account -> - messageAccounts.indexOfFirst { it == account.publicKey }.toByte() - } - - return CompiledInstruction( - programIndex = programIndex.toByte(), - accountIndexes = accountIndexes, - data = data - ) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Instruction - - if (program != other.program) return false - if (accounts != other.accounts) return false - if (data != other.data) return false - - return true - } - - override fun hashCode(): Int { - var result = program.hashCode() - result = 31 * result + accounts.hashCode() - result = 31 * result + data.hashCode() - return result - } - - companion object { - // newInstruction creates a new instruction. - fun newInstruction( - program: PublicKey, - data: List, - vararg accounts: AccountMeta, - ): Instruction { - return Instruction( - program, - accounts.asList(), - data - ) - } - } -} - -data class CompiledInstruction( - val programIndex: Byte, - val accountIndexes: List, - val data: List, -) { - val byteLength: Int by lazy { - 1 + - ShortVec.encodeLen(accountIndexes.size).size + - accountIndexes.size + - ShortVec.encodeLen(data.size).size + - data.size - } - - - fun encode(): List { - val container = mutableListOf() - container.add(programIndex) - container.addAll(ShortVec.encode(accountIndexes).toList()) - container.addAll(ShortVec.encode(data).toList()) - return container - } - - fun decompile(accounts: List): Instruction? { - if (accounts.size < accountIndexes.size + 1) { // +1 for program - return null - } - - val program = accounts[programIndex.toInt()].publicKey - val accountsD = accountIndexes.map { accounts[it.toInt()] } - - return Instruction( - program = program, - accounts = accountsD, - data = data - ) - } - - companion object { - fun fromList(list: List): CompiledInstruction? { - if (list.size <= 1) { - return null - } - - var payload = list - - val indexConsumed = payload.consume(1) - val index = indexConsumed.consumed.first() - payload = indexConsumed.remaining - - var (accountCount, accountData) = ShortVec.decodeLen(payload) - if (accountData.size < accountCount) return null - - val accountIndexesConsumed = accountData.consume(accountCount) - accountData = accountIndexesConsumed.remaining - val accountIndexes = accountIndexesConsumed.consumed - - val (opaqueCount, opaqueData) = ShortVec.decodeLen(accountData) - if(opaqueData.size < opaqueCount) return null - - return CompiledInstruction( - index, - accountIndexes, - opaqueData.prefix(opaqueCount) - ) - } - } -} - -val Instruction.description: String - get() = """ - ${program.base58()} ${accounts.count()} ${data.hexEncodedString()} - ${if (accounts.isNotEmpty()) accounts.joinToString { it.description } else ""} - """.trimIndent() diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/Message.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/Message.kt deleted file mode 100644 index e14ce8a08..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/Message.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.getcode.solana - -import com.getcode.solana.keys.filterUniqueAccounts -import com.getcode.utils.DataSlice.chunk -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.DataSlice.tail - -data class Message( - val header: MessageHeader, - val accounts: List, - var recentBlockhash: com.getcode.solana.keys.Hash, - val instructions: List, -) { - fun encode(): ByteArray { - val data = mutableListOf() - - val accounts = accounts.map { it.publicKey } - val instructions = instructions.map { it.compile(accounts) } - - data.addAll(header.encode().toList()) - data.addAll(ShortVec.encodeList(accounts.map { it.bytes })) - data.addAll(recentBlockhash.bytes) - data.addAll(ShortVec.encodeList(instructions.map { it.encode() })) - - return data.toByteArray() - } - - companion object { - fun newInstance(list: List): Message? { - var payload: List = list - - // Decode `header` - val headerConsumed = payload.consume(MessageHeader.length) - val header = MessageHeader.fromList(headerConsumed.consumed) - payload = headerConsumed.remaining - - // Decode `accountKeys` - val (accountCount, accountData) = ShortVec.decodeLen(payload) - val messageAccounts = accountData.chunk(com.getcode.solana.keys.LENGTH_32, accountCount) { - com.getcode.solana.keys.PublicKey( - it - ) - } - ?: return null - - payload = accountData.tail(com.getcode.solana.keys.LENGTH_32 * accountCount) - - // Decode `recentBlockHash` - val hashConsumed = payload.consume(com.getcode.solana.keys.LENGTH_32) - val hash = com.getcode.solana.keys.Hash(hashConsumed.consumed) - - payload = hashConsumed.remaining - - // Decode `instructions` - var (instructionCount, remainingData) = ShortVec.decodeLen(payload) - val compiledInstructions = mutableListOf() - - for (i in 0 until instructionCount) { - val instruction = CompiledInstruction.fromList(remainingData) ?: return null - - if (instruction.programIndex >= messageAccounts.size) { - return null - } - - remainingData = remainingData.tail(instruction.byteLength) - compiledInstructions.add(instruction) - } - - val metaAccounts = messageAccounts.mapIndexed { index, account -> - com.getcode.solana.keys.AccountMeta( - publicKey = account, - isSigner = index < header.requiredSignatures, - isWritable = index < header.requiredSignatures - header.readOnlySigners || - index >= header.requiredSignatures && index < messageAccounts.size - header.readOnly, - isPayer = index == 0, - isProgram = false - ) - } - - val instructions = compiledInstructions.mapNotNull { it.decompile(metaAccounts) } - - if (instructions.size != compiledInstructions.size) { - return null - } - - return Message(header, metaAccounts, hash, instructions) - } - - fun newInstance( - accounts: List, - recentBlockhash: com.getcode.solana.keys.Hash /* = com.getcode.solana.keys.Key32 */, - instructions: List - ): Message { - // Sort the account meta's based on: - // 1. Payer is always the first account / signer. - // 1. All signers are before non-signers. - // 2. Writable accounts before read-only accounts. - // 3. Programs last - val uniqueAccounts = accounts.filterUniqueAccounts().sorted() - - val signers = uniqueAccounts.filter { it.isSigner } - val readOnlySigners = uniqueAccounts.filter { !it.isWritable && it.isSigner } - val readOnly = uniqueAccounts.filter { !it.isWritable && !it.isSigner } - val header = MessageHeader( - requiredSignatures = signers.size, - readOnlySigners = readOnlySigners.size, - readOnly = readOnly.size - ) - - return Message( - header, - uniqueAccounts, - recentBlockhash, - instructions - ) - } - } -} diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/MessageHeader.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/MessageHeader.kt deleted file mode 100644 index 80bcd4d7b..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/MessageHeader.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.getcode.solana - - -open class MessageHeader( - val requiredSignatures: Int, - val readOnlySigners: Int, - val readOnly: Int -) { - fun encode(): ByteArray { - return byteArrayOf( - requiredSignatures.toByte(), - readOnlySigners.toByte(), - readOnly.toByte() - ) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MessageHeader - - if (requiredSignatures != other.requiredSignatures) return false - if (readOnlySigners != other.readOnlySigners) return false - if (readOnly != other.readOnly) return false - - return true - } - - override fun hashCode(): Int { - var result = requiredSignatures - result = 31 * result + readOnlySigners - result = 31 * result + readOnly - return result - } - - companion object { - const val length: Int = 3 - - fun fromList(list: List): MessageHeader { - val data = list.map { it.toInt() } - return MessageHeader( - requiredSignatures = data[0], - readOnlySigners = data[1], - readOnly = data[2] - ) - } - } -} - -val MessageHeader.description: String - get() = "H{$requiredSignatures, $readOnlySigners, $readOnly}" \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/ShortVec.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/ShortVec.kt deleted file mode 100644 index 1c468f057..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/ShortVec.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.getcode.solana - -import com.getcode.utils.DataSlice.tail -import java.io.ByteArrayInputStream - -object ShortVec { - /** - * decodeLen decodes a ShortVec encoded length from the [input]. - * - * @param input - the input stream that the length is encoded in - * @return - returns the decoded length of the ShortVec and Offset - */ - private fun decodeLen(input: ByteArrayInputStream): Pair { - var offset = 0 - val valBuf = ByteArray(1) - var value = 0 - - while (true) { - input.read(valBuf) - - value = value or (valBuf[0].toInt() and 0x7f shl (offset * 7)) - offset++ - - if ((valBuf[0].toInt() and 0x80) == 0) { - break - } - } - - return Pair(value, offset) - } - - /** - * decodeLen decodes a ShortVec encoded length from the [input]. - * - * @param input - the input list that the length is encoded in - * @return - returns the decoded length of the ShortVec and Offset - */ - fun decodeLen(input: List): Pair> { - val l = decodeLen(input.toByteArray().inputStream()) - return Pair(l.first, input.tail(l.second)) - } - - - /** - * encodeLen ShortVec encodes [length]. - * - * @param length - the length - * @return - returns ShortVec encoded length - */ - fun encodeLen(length: Int): List { - val data = mutableListOf() - var remaining = length - - while (true) { - var byte = (remaining and 0x7f).toByte() - remaining = remaining shr 7 - - if (remaining == 0) { - data.add(byte) - return data - } - - byte = (byte.toInt() or 0x80).toByte() - data.add(byte) - } - } - - /** - * encodeLen ShortVec encodes [list]. - * - * @param list - the input list - * @return - returns ShortVec encoded list - */ - fun encodeList(list: List>): List { - val container = encodeLen(list.size).toMutableList() - list.forEach { container.addAll(it) } - return container - } - - /** - * encodeLen ShortVec encodes [list]. - * - * @param list - the input list - * @return - returns ShortVec encoded list - */ - fun encode(list: List): List { - val container = encodeLen(list.size).toMutableList() - container.addAll(list) - return container - } -} diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/SolanaTransaction.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/SolanaTransaction.kt deleted file mode 100644 index e39146cbf..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/SolanaTransaction.kt +++ /dev/null @@ -1,223 +0,0 @@ -package com.getcode.solana - -import com.getcode.ed25519.Ed25519 -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.base58 -import com.getcode.solana.keys.description -import com.getcode.utils.DataSlice.chunk -import com.getcode.utils.DataSlice.tail -import com.getcode.utils.printDiff -import com.getcode.utils.printMatch - - -/* - Signature: [64]byte - PublicKey: [32]byte - Hash: [32]byte - CompiledInstruction: - program_id_index: byte // index of the program account in message::AccountKeys - accounts: short_vec // ordered indices mapping to message::AccountKeys to input to program - data: short_vec // raw data - Transaction: - signature: short_vec - Message: - Header: - num_required_signatures: byte - num_readonly_signed_accounts: byte - num_readonly_unsigned_accounts: byte - AccountKeys: short_vec - RecentBlockHash: Hash - Instructions: short_vec - Serialization: - - Arrays: No length, just elements. - - ShortVec: ShortVec encoded length, then elements - - Byte: Byte - - Structs: Fields are serialized in order as declared. No metadata about structs are serialized. -*/ - -data class SolanaTransaction(val message: Message, val signatures: List) { - val identifier - get() = signatures.first() - - var recentBlockhash - get() = message.recentBlockhash - set(v) { - message.recentBlockhash = v - } - - fun sign(vararg keyPairs: Ed25519.KeyPair): List { - val requiredSignatureCount = message.header.requiredSignatures - if (keyPairs.size > requiredSignatureCount) { - throw Exception(SigningError.tooManySigners.name) - } - - val messageData = message.encode() - val newSignatures = mutableListOf() - - keyPairs.forEach { keyPair -> - val signatureIndex = - message.accounts.indexOfFirst { it.publicKey.bytes == keyPair.publicKeyBytes.toList() } - if (signatureIndex == -1) { - throw Exception("accountNotInAccountList. Account: ${keyPair.publicKey}") - } - - val signature = Ed25519.sign(messageData, keyPair) - newSignatures.add(com.getcode.solana.keys.Signature(signature.toList())) - } - - return newSignatures - } - - fun encode(): List { - val data = mutableListOf() - data.addAll(ShortVec.encodeList(signatures.map { it.bytes })) - data.addAll(message.encode().toList()) - return data - } - - inline fun findInstruction(cb: (instruction: Instruction) -> com.getcode.solana.instructions.InstructionType): T? { - message.instructions.forEach { - try { - val res = cb(it) - if (res is T) { - return res - } - } catch (e: Exception) { - } - } - return null - } - - override fun toString(): String { - return """ - SolanaTransaction { - message=$message, - signatures=${signatures.joinToString { it.base58() }} - } - """.trimIndent() - } - - enum class SigningError { - tooManySigners, - accountNotInAccountList, - invalidKey - } - - companion object { - fun fromList(list: List): SolanaTransaction? { - val (signatureCount, payload) = ShortVec.decodeLen(list) - - if (payload.size < signatureCount * com.getcode.solana.keys.LENGTH_64) { - return null - } - - val signatures: List = - payload.chunk(size = com.getcode.solana.keys.LENGTH_64, count = signatureCount) { - com.getcode.solana.keys.Signature( - it - ) - }.orEmpty() - val messageData = payload.tail(signatureCount * com.getcode.solana.keys.LENGTH_64) - val message = Message.newInstance(messageData) ?: return null - - return SolanaTransaction(signatures = signatures.toMutableList(), message = message) - } - - fun newInstance( - payer: com.getcode.solana.keys.PublicKey, - recentBlockhash: Hash?, - instructions: List - ): SolanaTransaction { - val accounts = mutableListOf() - accounts.add(com.getcode.solana.keys.AccountMeta.payer(publicKey = payer)) - - instructions.forEach { - accounts.add(com.getcode.solana.keys.AccountMeta.program(publicKey = it.program)) - accounts.addAll(it.accounts) - } - - val message = Message.newInstance( - accounts = accounts, - recentBlockhash = recentBlockhash ?: Hash.zero, - instructions = instructions - ) - - val signatures = mutableListOf() - .apply { - for (i in 0 until message.header.requiredSignatures) { - add(com.getcode.solana.keys.Signature.zero) - } - } - - return SolanaTransaction( - message, - signatures - ) - } - } -} - -fun SolanaTransaction.diff(other: SolanaTransaction) { - val lhs = this - val rhs = other - - if (lhs.identifier == rhs.identifier) { - printMatch("ID") - } else { - printDiff( - title = "ID", - one = lhs.identifier.base58(), - two = rhs.identifier.base58() - ) - } - - if (lhs.signatures == rhs.signatures) { - printMatch("Signatures") - } else { - printDiff( - title = "Signatures", - one = lhs.signatures.map { it.base58() }, - two = rhs.signatures.map { it.base58() } - ) - } - - if (lhs.message.header == rhs.message.header) { - printMatch("Header") - } else { - printDiff( - title = "Header", - one = lhs.message.header.description, - two = rhs.message.header.description, - ) - } - - if (lhs.message.recentBlockhash == rhs.message.recentBlockhash) { - printMatch("Recent Blockhash") - } else { - printDiff( - title = "Recent Blockhash", - one = lhs.recentBlockhash.base58(), - two = rhs.recentBlockhash.base58(), - ) - } - - if (lhs.message.accounts == rhs.message.accounts) { - printMatch("Accounts") - } else { - printDiff( - title = "Accounts", - one = lhs.message.accounts.map { it.description }, - two = rhs.message.accounts.map { it.description }, - ) - } - - if (lhs.message.instructions == rhs.message.instructions) { - printMatch("Instructions") - } else { - printDiff( - title = "Instructions", - one = lhs.message.instructions.map { it.description }, - two = rhs.message.instructions.map { it.description }, - ) - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/AssociatedTokenAccounts.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/AssociatedTokenAccounts.kt deleted file mode 100644 index d2e9875ec..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/AssociatedTokenAccounts.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.getcode.solana.instructions - -import com.getcode.solana.instructions.programs.AssociatedTokenProgram -import com.getcode.solana.instructions.programs.SysVar -import com.getcode.solana.instructions.programs.SystemProgram -import com.getcode.solana.instructions.programs.TokenProgram -import com.getcode.solana.keys.PublicKey -import com.solana.transaction.AccountMeta -import com.solana.transaction.TransactionInstruction - - -// Function to create an Associated Token Account instruction -fun createAssociatedTokenAccountInstruction( - mint: PublicKey, // Token mint (e.g., USDC) - ata: PublicKey, // Associated Token Account (ATA) - payer: PublicKey, // Wallet paying for the account creation (sender) - owner: PublicKey, // Wallet to create ATA for (sender or recipient) - programId: PublicKey = TokenProgram.address -): TransactionInstruction { - return TransactionInstruction( - programId = AssociatedTokenProgram.address.asSolanaPublicKey(), - accounts = listOf( - AccountMeta(payer.asSolanaPublicKey(), isSigner = true, isWritable = true), // Payer (sender) - AccountMeta(ata.asSolanaPublicKey(), isSigner = false, isWritable = true), // ATA to create - AccountMeta(owner.asSolanaPublicKey(), isSigner = false, isWritable = false), // Wallet address - AccountMeta(mint.asSolanaPublicKey(), isSigner = false, isWritable = false), // Token mint - AccountMeta(SystemProgram.address.asSolanaPublicKey(), isSigner = false, isWritable = false), // System Program - AccountMeta(programId.asSolanaPublicKey(), isSigner = false, isWritable = false), // Token Program - AccountMeta(SysVar.rent.address().asSolanaPublicKey(), isSigner = false, isWritable = false) // Rent Sysvar - ), - data = byteArrayOf() - ) -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/InstructionType.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/InstructionType.kt deleted file mode 100644 index 4cf47ac5e..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/InstructionType.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.getcode.solana.instructions - -import com.getcode.solana.Instruction -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.DataSlice -import com.getcode.utils.DataSlice.consume - -interface ProgramType { - val address: PublicKey -} - -abstract class CommandType>: ProgramType { - abstract val commandByteLength: Int - abstract fun commandLookup(bytes: ByteArray): Definition? - fun ensure(instruction: Instruction, command: Definition) { - val stride = commandByteLength - if (instruction.data.size < stride) { - throw CommandParseError.CommandNotFound() - } - - val bytes = ByteArray(stride) { 0 } - instruction.data.take(stride).toByteArray().copyInto(bytes) - - val parsedCommand = commandLookup(bytes) - if (parsedCommand != command) { - throw CommandParseError.PayloadNotFound() - } - } - - fun parse(command: Definition, instruction: Instruction, expectingAccounts: Int?): DataSlice.ByteListConsume { - if (instruction.program != address) throw CommandParseError.InstructionMismatch() - val ensured = ensure(instruction, command) - - if (expectingAccounts != null) { - if (instruction.accounts.count() != expectingAccounts) { - throw CommandParseError.AccountMismatch() - } - } - - return instruction.data.consume(commandByteLength) - } -} - - - -interface InstructionType { - fun instruction(): Instruction - fun encode(): List -} - -inline fun > commandByteLength(): Int { - return java.lang.Long.SIZE / java.lang.Byte.SIZE -} - -sealed class CommandParseError : Exception() { - class InstructionMismatch: CommandParseError() - class CommandNotFound: CommandParseError() - class PayloadNotFound: CommandParseError() - class AccountMismatch: CommandParseError() -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/SplTransfer.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/SplTransfer.kt deleted file mode 100644 index 03d2e45be..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/SplTransfer.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.getcode.solana.instructions - -import com.getcode.solana.instructions.programs.TokenProgram -import com.getcode.solana.keys.Mint -import com.getcode.solana.keys.PublicKey -import com.solana.publickey.SolanaPublicKey -import com.solana.transaction.AccountMeta -import com.solana.transaction.TransactionInstruction -import java.nio.ByteBuffer -import java.nio.ByteOrder - - -fun createSplTransfer( - sender: PublicKey, - sourceTokenAccount: PublicKey, - destinationTokenAccount: PublicKey, - quarks: Long, -): TransactionInstruction { - // Create the TransactionInstruction - return TransactionInstruction( - programId = TokenProgram.address.asSolanaPublicKey(), - accounts = listOf( - AccountMeta(sourceTokenAccount.asSolanaPublicKey(), isSigner = false, isWritable = true), // Source token account - AccountMeta(destinationTokenAccount.asSolanaPublicKey(), isSigner = false, isWritable = true), // Destination token account - AccountMeta(sender.asSolanaPublicKey(), isSigner = true, isWritable = true) // Authority (sender's wallet) - ), - data = ByteBuffer.allocate(9).apply { - order(ByteOrder.LITTLE_ENDIAN) - put(3.toByte()) - putLong(quarks) - }.array() - ) -} - -fun createSplCheckedTransfer( - sender: PublicKey, - mint: Mint, - sourceTokenAccount: PublicKey, - destinationTokenAccount: PublicKey, - quarks: Long, - decimals: Int, -): TransactionInstruction { - // Create the TransactionInstruction - return TransactionInstruction( - programId = TokenProgram.address.asSolanaPublicKey(), - accounts = listOf( - AccountMeta(sourceTokenAccount.asSolanaPublicKey(), isSigner = false, isWritable = true), // Source token account - AccountMeta(mint.asSolanaPublicKey(), isSigner = false, isWritable = false), // Mint to check against - AccountMeta(destinationTokenAccount.asSolanaPublicKey(), isSigner = false, isWritable = true), // Destination token account - AccountMeta(sender.asSolanaPublicKey(), isSigner = true, isWritable = true) // Authority (sender's wallet) - ), - data = ByteBuffer.allocate(10).apply { - order(ByteOrder.LITTLE_ENDIAN) - put(12.toByte()) - putLong(quarks) - put(decimals.toByte()) - }.array() - ) -} - -internal fun PublicKey.asSolanaPublicKey() = SolanaPublicKey(byteArray) \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgram.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgram.kt deleted file mode 100644 index 4e2c2926c..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgram.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.vendor.Base58 - -class AssociatedTokenProgram { - companion object { - val address = com.getcode.solana.keys.PublicKey( - Base58.decode("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").toList() - ) - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgram_CreateAccount.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgram_CreateAccount.kt deleted file mode 100644 index 4e99c072d..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgram_CreateAccount.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction - -data class TimelockAccounts(val state: com.getcode.solana.keys.PublicKey, val vault: com.getcode.solana.keys.PublicKey) - - /// Create an associated token account for the given wallet address and token mint - /// Accounts expected by this instruction: - /// - /// 0. `[writeable,signer]` Funding account (must be a system account) - /// 1. `[writeable]` Associated token account address to be created - /// 2. `[]` Wallet address for the new associated token account - /// 3. `[]` The token mint for the new associated token account - /// 4. `[]` System program - /// 5. `[]` SPL Token program - /// 6. `[]` Rent sysvar - /// - /// Reference: - /// https://github.com/solana-labs/solana-program-library/blob/0639953c7dd0f5228c3ceda3ba68fece3b46ff1d/associated-token-account/program/src/lib.rs#L54 - /// - -class AssociatedTokenProgram_CreateAccount( - val subsidizer: com.getcode.solana.keys.PublicKey, - val owner: com.getcode.solana.keys.PublicKey, - val associatedTokenAccount: com.getcode.solana.keys.PublicKey, - val mint: com.getcode.solana.keys.PublicKey -): com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - return Instruction( - program = AssociatedTokenProgram.address, - accounts = listOf( - AccountMeta.writable(publicKey = subsidizer, signer = true), - AccountMeta.writable(publicKey = associatedTokenAccount), - AccountMeta.readonly(publicKey = owner), - AccountMeta.readonly(publicKey = mint), - AccountMeta.readonly(publicKey = SystemProgram.address), - AccountMeta.readonly(publicKey = TokenProgram.address), - AccountMeta.readonly(publicKey = SysVar.rent.address()), - ), - data = encode() - ) - } - - override fun encode(): List { - return listOf() - } - - companion object { - fun newInstance(instruction: Instruction): AssociatedTokenProgram_CreateAccount { - if (instruction.accounts.size != 7) throw Exception() - - val accounts = instruction.accounts.map { it.publicKey } - - return AssociatedTokenProgram_CreateAccount( - subsidizer = accounts[0], - owner = accounts[1], - associatedTokenAccount = accounts[2], - mint = accounts[3], - ) - } - } -} diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram.kt deleted file mode 100644 index 6bb0209ae..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.vendor.Base58 - -class ComputeBudgetProgram { - - enum class Command(val value: Byte) { - requestUnits(0), - requestHeapFrame(1), - setComputeUnitLimit(2), - setComputeUnitPrice(3), - } - - companion object: com.getcode.solana.instructions.CommandType() { - override val address = com.getcode.solana.keys.PublicKey( - Base58.decode("ComputeBudget111111111111111111111111111111").toList() - ) - - override val commandByteLength: Int - get() = Byte.SIZE_BYTES - - override fun commandLookup(bytes: ByteArray): Command? { - return Command.entries.firstOrNull { it.value == bytes.first() } - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_RequestUnits.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_RequestUnits.kt deleted file mode 100644 index cdb00a185..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_RequestUnits.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.Instruction -import com.getcode.solana.instructions.programs.ComputeBudgetProgram.Command -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.byteArrayToLong -import com.getcode.utils.intToByteArray -import com.getcode.utils.longToByteArray - -class ComputeBudgetProgram_RequestUnits( - val limit: Long, - val bump: Byte, -): com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - return Instruction( - program = ComputeBudgetProgram.address, - accounts = emptyList(), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.addAll(Command.requestUnits.ordinal.intToByteArray().toList()) - data.add(bump) - data.addAll(limit.longToByteArray().toList()) - return data - } - - companion object { - fun newInstance(instruction: Instruction): ComputeBudgetProgram_RequestUnits { - val data = ComputeBudgetProgram.parse( - command = Command.requestUnits, - instruction = instruction, - expectingAccounts = 0 - ) - - - val bump = data.remaining.consume(1) - val limit = bump.remaining.consume(8).consumed.toByteArray().byteArrayToLong() - - return ComputeBudgetProgram_RequestUnits( - bump = bump.consumed.first(), - limit = limit - ) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_SetComputeUnitLimit.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_SetComputeUnitLimit.kt deleted file mode 100644 index f73af5da0..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_SetComputeUnitLimit.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.Instruction -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.byteArrayToInt -import com.getcode.utils.intToByteArray - -class ComputeBudgetProgram_SetComputeUnitLimit( - val limit: Int, - val bump: Byte, -): com.getcode.solana.instructions.InstructionType { - - override fun instruction(): Instruction { - return Instruction( - program = ComputeBudgetProgram.address, - accounts = emptyList(), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.add(ComputeBudgetProgram.Command.setComputeUnitLimit.ordinal.toByte()) -// data.add(bump) - data.addAll(limit.intToByteArray().toList()) - return data - } - - companion object { - fun newInstance(instruction: Instruction): ComputeBudgetProgram_SetComputeUnitLimit { - val data = ComputeBudgetProgram.parse( - command = ComputeBudgetProgram.Command.setComputeUnitLimit, - instruction = instruction, - expectingAccounts = 0 - ) - - val stride = UInt.SIZE_BYTES - val limit = data.remaining.consume(stride).consumed.toByteArray().byteArrayToInt() - - return ComputeBudgetProgram_SetComputeUnitLimit(bump = data.consumed.first(), limit = limit) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_SetComputeUnitPrice.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_SetComputeUnitPrice.kt deleted file mode 100644 index 41520dca1..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgram_SetComputeUnitPrice.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.Instruction -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.byteArrayToLong -import com.getcode.utils.longToByteArray - -class ComputeBudgetProgram_SetComputeUnitPrice( - val microLamports: Long, - val bump: Byte, -): com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - return Instruction( - program = ComputeBudgetProgram.address, - accounts = emptyList(), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.add(ComputeBudgetProgram.Command.setComputeUnitPrice.ordinal.toByte()) -// data.add(bump) - data.addAll(microLamports.longToByteArray().toList()) - return data - } - - companion object { - fun newInstance(instruction: Instruction): ComputeBudgetProgram_SetComputeUnitPrice { - val data = ComputeBudgetProgram.parse( - command = ComputeBudgetProgram.Command.setComputeUnitPrice, - instruction = instruction, - expectingAccounts = 0 - ) - val microLamports = data.remaining.consume(8).consumed.toByteArray().byteArrayToLong() - - return ComputeBudgetProgram_SetComputeUnitPrice(bump = data.consumed.first(), microLamports = microLamports) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/MemoProgram.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/MemoProgram.kt deleted file mode 100644 index 777024a27..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/MemoProgram.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.vendor.Base58 - -/// Send tokens from one accounts to another. Accounts expected by this instruction: -/// -/// ## Single owner/delegate -/// -/// 0. `[writable]` The source account. -/// 1. `[writable]` The destination account. -/// 2. `[signer]` The source account's owner/delegate. -/// -/// ## Multisignature owner/delegate -/// -/// 0. `[writable]` The source account. -/// 1. `[writable]` The destination account. -/// 2. `[]` The source account's multisignature owner/delegate. -/// 3. ..3+M `[signer]` M signer accounts. -/// -/// Reference: -/// https://github.com/solana-labs/solana-program-library/blob/b011698251981b5a12088acba18fad1d41c3719a/token/program/src/instruction.rs#L76-L91 -/// - -open class MemoProgram(val data: List) { - companion object { - val address = com.getcode.solana.keys.PublicKey( - Base58.decode("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo").toList() - ) - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram.kt deleted file mode 100644 index 7cf43e877..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.utils.DataSlice.toLong -import com.getcode.vendor.Base58 - -class SwapValidatorProgram { - enum class Command(val value: Long) { - preSwap("717F49CF8AC7DDB7".toULong(16).toLong()), - postSwap("A1758AB339B7D59F".toULong(16).toLong()) - } - - companion object: com.getcode.solana.instructions.CommandType() { - override val address = com.getcode.solana.keys.PublicKey( - Base58.decode("sWvA66HNNvgamibZe88v3NN5nQwE8tp3KitfViFjukA").toList() - ) - override val commandByteLength: Int get() = Long.SIZE_BYTES - - override fun commandLookup(bytes: ByteArray): Command? { - return Command.entries.firstOrNull { it.value == bytes.toLong() } - } - } -} - diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram_PostSwap.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram_PostSwap.kt deleted file mode 100644 index 1c68e3002..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram_PostSwap.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.DataSlice.toLong -import com.getcode.utils.toByteArray - -class SwapValidatorProgram_PostSwap( - val stateBump: Byte, - val maxToSend: Long, - val minToReceive: Long, - val preSwapState: com.getcode.solana.keys.PublicKey, - val source: com.getcode.solana.keys.PublicKey, - val destination: com.getcode.solana.keys.PublicKey, - val payer: com.getcode.solana.keys.PublicKey, -) : com.getcode.solana.instructions.InstructionType { - - override fun instruction(): Instruction { - return Instruction( - program = SwapValidatorProgram.address, - accounts = listOf( - AccountMeta.writable(publicKey = preSwapState), - AccountMeta.readonly(publicKey = source), - AccountMeta.readonly(publicKey = destination), - AccountMeta.writable(publicKey = payer, signer = true), - ), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.addAll(SwapValidatorProgram.Command.postSwap.value.toByteArray().toList()) - data.add(stateBump) - data.addAll(maxToSend.toByteArray().toList()) - data.addAll(minToReceive.toByteArray().toList()) - - return data - } - - companion object { - fun newInstance(instruction: Instruction): SwapValidatorProgram_PostSwap { - val data = SwapValidatorProgram.parse(SwapValidatorProgram.Command.postSwap, instruction, null) - - val stateStride = Byte.SIZE_BYTES - val (stateBump, bumpRemaining) = data.remaining.consume(stateStride).let { - it.consumed.toByteArray().toLong().toByte() to it.remaining - } - - val maxToSendStride = ULong.SIZE_BYTES - val (maxToSend, maxToSendRemaining) = bumpRemaining.consume(maxToSendStride).let { - it.consumed.toByteArray().toLong() to it.remaining - } - - val minToReceiveStride = ULong.SIZE_BYTES - val (minToReceive, _) = maxToSendRemaining.consume(minToReceiveStride).let { - it.consumed.toByteArray().toLong() to it.remaining - } - - return SwapValidatorProgram_PostSwap( - stateBump = stateBump, - maxToSend = maxToSend, - minToReceive = minToReceive, - preSwapState = instruction.accounts[0].publicKey, - source = instruction.accounts[1].publicKey, - destination = instruction.accounts[2].publicKey, - payer = instruction.accounts[3].publicKey, - ) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram_PreSwap.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram_PreSwap.kt deleted file mode 100644 index 52ffa8293..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgram_PreSwap.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction -import com.getcode.utils.toByteArray - -class SwapValidatorProgram_PreSwap( - val preSwapState: com.getcode.solana.keys.PublicKey, - val user: com.getcode.solana.keys.PublicKey, - val source: com.getcode.solana.keys.PublicKey, - val destination: com.getcode.solana.keys.PublicKey, - val nonce: com.getcode.solana.keys.PublicKey, - val payer: com.getcode.solana.keys.PublicKey, - val remainingAccounts: List, -): com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - val accounts = mutableListOf( - AccountMeta.writable(publicKey = preSwapState), - AccountMeta.readonly(publicKey = user), - AccountMeta.readonly(publicKey = source), - AccountMeta.readonly(publicKey = destination), - AccountMeta.readonly(publicKey = nonce), - AccountMeta.writable(publicKey = payer, signer = true), - AccountMeta.readonly(publicKey = SystemProgram.address), - AccountMeta.readonly(publicKey = SysVar.rent.address()) - ) - - accounts.addAll(remainingAccounts) - - return Instruction( - program = SwapValidatorProgram.address, - accounts = accounts, - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.addAll(SwapValidatorProgram.Command.preSwap.value.toByteArray().toList()) - return data - } - - companion object { - fun newInstance(instruction: Instruction): SwapValidatorProgram_PreSwap { - SwapValidatorProgram.parse( - command = SwapValidatorProgram.Command.preSwap, - instruction = instruction, - expectingAccounts = null - ) - - return SwapValidatorProgram_PreSwap( - preSwapState = instruction.accounts[0].publicKey, - user = instruction.accounts[1].publicKey, - source = instruction.accounts[2].publicKey, - destination = instruction.accounts[3].publicKey, - nonce = instruction.accounts[4].publicKey, - payer = instruction.accounts[5].publicKey, - remainingAccounts = instruction.accounts.drop(6) - ) - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SysVar.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SysVar.kt deleted file mode 100644 index 4008036cc..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SysVar.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.decodeBase58 - -enum class SysVar(val value: String) { - clock ("SysvarC1ock11111111111111111111111111111111"), - epochSchedule ("SysvarEpochSchedu1e111111111111111111111111"), - fees ("SysvarFees111111111111111111111111111111111"), - instructions ("Sysvar1nstructions1111111111111111111111111"), - recentBlockhashes ("SysvarRecentB1ockHashes11111111111111111111"), - rent ("SysvarRent111111111111111111111111111111111"), - slotHashes ("SysvarS1otHashes111111111111111111111111111"), - slotHistory ("SysvarS1otHistory11111111111111111111111111"), - stackHistory ("SysvarStakeHistory1111111111111111111111111"); - - fun address(): PublicKey = - PublicKey(this.value.decodeBase58().toList()) -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SystemProgram.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SystemProgram.kt deleted file mode 100644 index 42c79844a..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SystemProgram.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.LENGTH_32 -import com.getcode.solana.keys.PublicKey - -class SystemProgram { - - enum class Command { - createAccount, - assign, - transfer, - createAccountWithSeed, - advanceNonceAccount, - withdrawNonceAccount, - initializeNonceAccount, - authorizeNonceAccount, - allocate, - allocateWithSeed, - assignWithSeed, - transferWithSeed, - } - - companion object { - val address = - PublicKey(ByteArray(LENGTH_32) { 0 }.toList()) - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SystemProgram_AdvanceNonce.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SystemProgram_AdvanceNonce.kt deleted file mode 100644 index 48292cbcd..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/SystemProgram_AdvanceNonce.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction -import com.getcode.utils.intToByteArray - -class SystemProgram_AdvanceNonce( - val nonce: com.getcode.solana.keys.PublicKey, - val authority: com.getcode.solana.keys.PublicKey, -) : com.getcode.solana.instructions.InstructionType { - override fun instruction(): Instruction { - return Instruction( - program = SystemProgram.address, - accounts = listOf( - AccountMeta.writable(publicKey = nonce), - AccountMeta.readonly(publicKey = SysVar.recentBlockhashes.address()), - AccountMeta.readonly(publicKey = authority, signer = true), - ), - data = encode() - ) - } - - override fun encode(): List { - val data = mutableListOf() - data.addAll(SystemProgram.Command.advanceNonceAccount.ordinal.intToByteArray().toList()) - return data - } - - companion object { - fun newInstance(instruction: Instruction): SystemProgram_AdvanceNonce { - return SystemProgram_AdvanceNonce( - nonce = instruction.accounts[0].publicKey, - authority = instruction.accounts[2].publicKey - ) - - } - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TokenProgram.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TokenProgram.kt deleted file mode 100644 index e87030fc2..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/instructions/programs/TokenProgram.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.vendor.Base58 - -class TokenProgram { - companion object { - val address = com.getcode.solana.keys.PublicKey( - Base58.decode("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").toList() - ) - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/transactions/Message.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/transactions/Message.kt deleted file mode 100644 index f5841f93a..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/transactions/Message.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.getcode.solana.transactions - -import com.getcode.solana.keys.AccountMeta.Companion.compareLexicographically -import com.solana.publickey.SolanaPublicKey -import com.solana.transaction.AccountMeta -import com.solana.transaction.AddressTableLookup -import com.solana.transaction.Instruction -import com.solana.transaction.LegacyMessage -import com.solana.transaction.Message -import com.solana.transaction.VersionedMessage -import com.solana.transaction.toUnsignedTransaction - -fun Message.Builder.buildPatched(): Message { - check(blockhash != null) - val writableIndexes = mutableListOf() - val readableIndexes = mutableListOf() - - val writableSigners = mutableSetOf() - val readOnlySigners = mutableSetOf() - val writableNonSigners = mutableSetOf() - val readOnlyNonSigners = mutableSetOf() - val programIds = mutableSetOf() - - instructions.forEach { instruction -> - instruction.accounts.forEachIndexed { index, account -> - if (account.isSigner) { - if (account.isWritable) writableSigners.add(account.publicKey) - else { - readOnlySigners.add(account.publicKey) - } - } else { - if (account.isWritable) writableNonSigners.add(account.publicKey) - else readOnlyNonSigners.add(account.publicKey) - } - - if (account.isWritable) { - writableIndexes += index.toUByte() - } else { - readableIndexes += index.toUByte() - } - } - programIds.add(instruction.programId) - } - - val signers = writableSigners + readOnlySigners - - // add program ids after everything else - val accounts = signers + writableNonSigners + readOnlyNonSigners + programIds - - val compiledInstructions = instructions.map { instruction -> - Instruction( - accounts.indexOfFirst { it == instruction.programId }.toUByte(), - accounts.map { account -> - accounts.indexOfFirst { it == account }.toUByte() - }.toUByteArray().toByteArray(), - instruction.data - ) - } - - return LegacyMessage( - signers.size.toUByte(), - readOnlySigners.size.toUByte(), - readOnlyNonSigners.size.toUByte(), - accounts.toList(), - blockhash!!, - compiledInstructions - ) -} - -fun Message.inspect() { - val transaction = toUnsignedTransaction() - println("############################") - println("Inspecting Transaction") - println("Recent Blockhash: ${transaction.message.blockhash}") - println("Signature Count: ${transaction.message.signatureCount}") - println("Read-Only Signers: ${transaction.message.readOnlyAccounts}") - println("Read-Only Non-Signers: ${transaction.message.readOnlyNonSigners}") - - // Infer account flags from Message structure - val totalAccounts = transaction.message.accounts.size - val writableSignersCount = transaction.message.signatureCount.toInt() - transaction.message.readOnlyAccounts.toInt() - val readOnlySignersStart = writableSignersCount - val readOnlySignersEnd = readOnlySignersStart + transaction.message.readOnlyAccounts.toInt() - val writableNonSignersEnd = totalAccounts - transaction.message.readOnlyNonSigners.toInt() - - // Inspect instructions - println("\nInstructions:") - transaction.message.instructions.forEachIndexed { index, instruction -> - println(" Instruction $index:") - println(" Program ID Index: ${instruction.programIdIndex}") - val programId = transaction.message.accounts.getOrNull(instruction.programIdIndex.toInt()) - if (programId == null) { - println(" ERROR: Invalid program ID index ${instruction.programIdIndex} (out of bounds for $totalAccounts accounts)") - } else { - println(" Program ID: $programId") - } - println(" Account Indices: ${instruction.accountIndices.joinToString { it.toInt().toString() }}") - println(" Instruction Data: ${instruction.data.joinToString { it.toUByte().toString(16).padStart(2, '0') }}") - // Validate account indices - instruction.accountIndices.forEachIndexed { accIndex, indexByte -> - val accIndexInt = indexByte.toInt() - if (accIndexInt < 0 || accIndexInt >= totalAccounts) { - println(" ERROR: Invalid account index $accIndexInt at position $accIndex (out of bounds for $totalAccounts accounts)") - } else { - val account = transaction.message.accounts[accIndexInt] - val isSigner = accIndexInt < transaction.message.signatureCount.toInt() - val isWritable = accIndexInt < writableNonSignersEnd - println(" Account Index $accIndexInt: $account (isSigner=$isSigner, isWritable=$isWritable)") - } - } - } - println("############################") -} - -// Provide a unique set by publicKey of AccountMeta -// with the highest write permission. -private fun List.filterUniqueAccounts(): List { - val container = mutableListOf() - this.forEach { account -> - var found = false - var index = 0 - - for (existingAccount in container) { - if (account.publicKey == existingAccount.publicKey) { - val updatedAccount = existingAccount.copy( - isSigner = account.isSigner, - isWritable = account.isWritable - ) - - container[index] = updatedAccount - found = true - break - } - index++ - } - - if (!found) { - container.add(account) - } - } - - return container -} - -private val AccountMetaComparator : Comparator = Comparator { left, right -> - fun boolToInt(value: Boolean) = if (value) -1 else 1 - - if (left.isSigner != right.isSigner) { - return@Comparator boolToInt(left.isSigner) - } - - if (left.isWritable != right.isWritable) { - return@Comparator boolToInt(left.isWritable) - } - - compareLexicographically(left.publicKey.bytes, right.publicKey.bytes) -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/utils/DataSlice.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/utils/DataSlice.kt deleted file mode 100644 index e89f1e047..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/utils/DataSlice.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.getcode.utils - -import kotlin.math.min - -object DataSlice { - data class ByteListConsume(val consumed: List, val remaining: List) - - fun List.canConsume(length: Int): Boolean { - return count() >= length - } - - fun List.consume(length: Int): ByteListConsume { - if (length <= 0) return ByteListConsume(listOf(), this) - val consumed = this.prefix(length) - val remaining = this.suffix(min(length, size)) - return ByteListConsume(consumed.toList(), remaining.toList()) - } - - fun List.prefix(toIndex: Int): List { - if (toIndex < 0 || toIndex > size) return listOf() - return this.subList(0, toIndex) - } - - fun List.suffix(fromIndex: Int): List { - if (fromIndex < 0 || fromIndex > size) return listOf() - return this.subList(fromIndex, this.size) - } - - fun List.tail(fromIndex: Int): List { - return suffix(fromIndex) - } - fun List.chunk(size: Int, count: Int, block: (List) -> T): List? { - val requestSize = size * count - if (requestSize > this.size) return null - - val container = mutableListOf() - - for (i in 0 until count) { - val index = i * size - val slice = this.subList(index, index + size) - container.add(block(slice)) - } - - return container - } - - fun Byte.byteToUnsignedInt() = if (this < 0) this + 256 else this.toInt() - - fun ByteArray.toLong(): Long { - require(size <= 8) { "ByteArray size must be 8 bytes or less" } - - var result: Long = 0 - for (i in indices) { - result = result or ((this[i].toLong() and 0xFF) shl (i * 8)) - } - - return result - } -} \ No newline at end of file diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/utils/Differ.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/utils/Differ.kt deleted file mode 100644 index 860a6d1eb..000000000 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/utils/Differ.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.getcode.utils - -import timber.log.Timber -import kotlin.math.max - -fun printDiff(title: String, one: String, two: String) { - printDiff(title, listOf(one), listOf(two)) -} - -fun printDiff(title: String, one: List, two: List) { - val lineSeparator = "\n -\n" - - Timber.d("✗ $title") - Timber.d("|----------------------------------------------------------------------") - - val lines = (0 until max(one.count(), two.count())).joinToString(lineSeparator) { i -> - var oneValue = "-" - if (i < one.count()) { - oneValue = one[i] - } - - var twoValue = "-" - if (i < two.count()) { - twoValue = two[i] - } - - val content = """ - | ${if (oneValue != twoValue) "✗" else "✓"} 1: $oneValue - | ${if (oneValue != twoValue) "✗" else "✓"} 2: $twoValue - """.trimIndent() - - content - } - - Timber.d(lines) - Timber.d("|----------------------------------------------------------------------") -} - -fun printMatch(title: String) { - Timber.d("✓ $title") -} \ No newline at end of file diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/InstructionTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/InstructionTest.kt deleted file mode 100644 index b320da3bd..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/InstructionTest.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.getcode.solana - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.keys.PublicKey -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull - -class InstructionTest { - - private fun testKey(index: Int): PublicKey { - return PublicKey(ByteArray(32) { index.toByte() }.toList()) - } - - // Instruction.compile tests - - @Test - fun compileProducesCorrectProgramIndex() { - val programKey = testKey(0) - val accountKey = testKey(1) - - val instruction = Instruction( - program = programKey, - accounts = listOf(AccountMeta.readonly(accountKey)), - data = listOf(0xAA.toByte()), - ) - - val messageAccounts = listOf(programKey, accountKey) - val compiled = instruction.compile(messageAccounts) - - assertEquals(0.toByte(), compiled.programIndex) - } - - @Test - fun compileProducesCorrectAccountIndexes() { - val programKey = testKey(0) - val account1 = testKey(1) - val account2 = testKey(2) - - val instruction = Instruction( - program = programKey, - accounts = listOf( - AccountMeta.readonly(account1), - AccountMeta.writable(account2), - ), - data = listOf(0x01), - ) - - val messageAccounts = listOf(programKey, account1, account2) - val compiled = instruction.compile(messageAccounts) - - assertEquals(listOf(1.toByte(), 2.toByte()), compiled.accountIndexes) - } - - @Test - fun compilePreservesData() { - val programKey = testKey(0) - val data = listOf(0x01, 0x02, 0x03) - - val instruction = Instruction( - program = programKey, - accounts = emptyList(), - data = data, - ) - - val compiled = instruction.compile(listOf(programKey)) - assertEquals(data, compiled.data) - } - - // CompiledInstruction encode/fromList roundtrip - - @Test - fun compiledInstructionEncodeFromListRoundtrip() { - val compiled = CompiledInstruction( - programIndex = 3.toByte(), - accountIndexes = listOf(0.toByte(), 1.toByte(), 2.toByte()), - data = listOf(0xAA.toByte(), 0xBB.toByte()), - ) - - val encoded = compiled.encode() - val decoded = CompiledInstruction.fromList(encoded) - - assertNotNull(decoded) - assertEquals(compiled.programIndex, decoded.programIndex) - assertEquals(compiled.accountIndexes, decoded.accountIndexes) - assertEquals(compiled.data, decoded.data) - } - - @Test - fun compiledInstructionEncodeFromListRoundtripEmptyData() { - val compiled = CompiledInstruction( - programIndex = 0.toByte(), - accountIndexes = listOf(1.toByte()), - data = emptyList(), - ) - - val encoded = compiled.encode() - val decoded = CompiledInstruction.fromList(encoded) - - assertNotNull(decoded) - assertEquals(compiled.programIndex, decoded.programIndex) - assertEquals(compiled.accountIndexes, decoded.accountIndexes) - assertEquals(compiled.data, decoded.data) - } - - // decompile tests - - @Test - fun decompileReversesCompile() { - val programKey = testKey(0) - val account1 = testKey(1) - val account2 = testKey(2) - - val originalInstruction = Instruction( - program = programKey, - accounts = listOf( - AccountMeta.readonly(account1), - AccountMeta.writable(account2), - ), - data = listOf(0x42), - ) - - val messageAccounts = listOf(programKey, account1, account2) - val accountMetas = listOf( - AccountMeta.program(programKey), - AccountMeta.readonly(account1), - AccountMeta.writable(account2), - ) - - val compiled = originalInstruction.compile(messageAccounts) - val decompiled = compiled.decompile(accountMetas) - - assertNotNull(decompiled) - assertEquals(originalInstruction.program, decompiled.program) - assertEquals(originalInstruction.data, decompiled.data) - assertEquals(originalInstruction.accounts.size, decompiled.accounts.size) - } - - @Test - fun decompileReturnsNullWhenInsufficientAccounts() { - val compiled = CompiledInstruction( - programIndex = 0.toByte(), - accountIndexes = listOf(1.toByte(), 2.toByte()), - data = listOf(0x01), - ) - - // Only one account, but we need at least 3 (program + 2 account indexes) - val accounts = listOf(AccountMeta.program(testKey(0))) - val result = compiled.decompile(accounts) - assertNull(result) - } - - // fromList edge cases - - @Test - fun fromListWithEmptyDataReturnsNull() { - val result = CompiledInstruction.fromList(emptyList()) - assertNull(result) - } - - @Test - fun fromListWithSingleByteReturnsNull() { - val result = CompiledInstruction.fromList(listOf(0.toByte())) - assertNull(result) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/MessageHeaderTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/MessageHeaderTest.kt deleted file mode 100644 index f24591bcf..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/MessageHeaderTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.getcode.solana - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class MessageHeaderTest { - - @Test - fun encodeProducesThreeBytes() { - val header = MessageHeader( - requiredSignatures = 2, - readOnlySigners = 1, - readOnly = 3, - ) - val encoded = header.encode() - assertEquals(3, encoded.size) - } - - @Test - fun encodeContainsCorrectValues() { - val header = MessageHeader( - requiredSignatures = 2, - readOnlySigners = 1, - readOnly = 3, - ) - val encoded = header.encode() - assertEquals(2.toByte(), encoded[0]) - assertEquals(1.toByte(), encoded[1]) - assertEquals(3.toByte(), encoded[2]) - } - - @Test - fun encodeFromListRoundtrip() { - val header = MessageHeader( - requiredSignatures = 5, - readOnlySigners = 2, - readOnly = 4, - ) - val encoded = header.encode() - val decoded = MessageHeader.fromList(encoded.toList()) - - assertEquals(header.requiredSignatures, decoded.requiredSignatures) - assertEquals(header.readOnlySigners, decoded.readOnlySigners) - assertEquals(header.readOnly, decoded.readOnly) - } - - @Test - fun fromListDecodesCorrectly() { - val bytes = listOf(10, 3, 7) - val header = MessageHeader.fromList(bytes) - - assertEquals(10, header.requiredSignatures) - assertEquals(3, header.readOnlySigners) - assertEquals(7, header.readOnly) - } - - @Test - fun equality() { - val header1 = MessageHeader( - requiredSignatures = 1, - readOnlySigners = 2, - readOnly = 3, - ) - val header2 = MessageHeader( - requiredSignatures = 1, - readOnlySigners = 2, - readOnly = 3, - ) - assertEquals(header1, header2) - } - - @Test - fun headerLengthConstant() { - assertEquals(3, MessageHeader.length) - } - - @Test - fun encodeZeroValues() { - val header = MessageHeader( - requiredSignatures = 0, - readOnlySigners = 0, - readOnly = 0, - ) - val encoded = header.encode() - assertTrue(encoded.all { it == 0.toByte() }) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/MessageTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/MessageTest.kt deleted file mode 100644 index 5279a25c2..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/MessageTest.kt +++ /dev/null @@ -1,182 +0,0 @@ -package com.getcode.solana - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.PublicKey -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull - -class MessageTest { - - private fun testKey(seed: Int): PublicKey = - PublicKey(ByteArray(32) { seed.toByte() }.toList()) - - private fun testHash(): Hash = - Hash(ByteArray(32) { 0xAB.toByte() }.toList()) - - // --- newInstance from accounts --- - - @Test - fun constructsHeaderFromAccounts() { - val payer = AccountMeta.payer(testKey(1)) - val writable = AccountMeta.writable(testKey(2)) - val readonly = AccountMeta.readonly(testKey(3)) - val program = AccountMeta(testKey(4), isSigner = false, isWritable = false, isPayer = false, isProgram = true) - - val message = Message.newInstance( - accounts = listOf(payer, writable, readonly, program), - recentBlockhash = testHash(), - instructions = emptyList() - ) - - // payer is the only signer - assertEquals(1, message.header.requiredSignatures) - } - - @Test - fun payerIsFirstAccount() { - val payer = AccountMeta.payer(testKey(1)) - val writable = AccountMeta.writable(testKey(2)) - - val message = Message.newInstance( - accounts = listOf(writable, payer), - recentBlockhash = testHash(), - instructions = emptyList() - ) - - assertEquals(testKey(1), message.accounts.first().publicKey) - } - - // --- encode / decode roundtrip --- - - @Test - fun encodeDecodeRoundtripNoInstructions() { - val payer = AccountMeta.payer(testKey(1)) - - val original = Message.newInstance( - accounts = listOf(payer), - recentBlockhash = testHash(), - instructions = emptyList() - ) - - val encoded = original.encode() - val decoded = Message.newInstance(encoded.toList()) - - assertNotNull(decoded) - assertEquals(original.header, decoded.header) - assertEquals(original.recentBlockhash, decoded.recentBlockhash) - assertEquals(original.accounts.size, decoded.accounts.size) - assertEquals(original.instructions.size, decoded.instructions.size) - } - - @Test - fun encodeDecodeRoundtripWithInstruction() { - val payer = AccountMeta.payer(testKey(1)) - val dest = AccountMeta.writable(testKey(2)) - val program = AccountMeta(testKey(3), isSigner = false, isWritable = false, isPayer = false, isProgram = true) - - val instruction = Instruction( - program = testKey(3), - accounts = listOf(payer, dest), - data = listOf(0x01, 0x02, 0x03) - ) - - val original = Message.newInstance( - accounts = listOf(payer, dest, program), - recentBlockhash = testHash(), - instructions = listOf(instruction) - ) - - val encoded = original.encode() - val decoded = Message.newInstance(encoded.toList()) - - assertNotNull(decoded) - assertEquals(original.header, decoded.header) - assertEquals(1, decoded.instructions.size) - assertEquals(listOf(0x01, 0x02, 0x03), decoded.instructions[0].data) - } - - @Test - fun encodeDecodePreservesBlockhash() { - val hash = testHash() - val payer = AccountMeta.payer(testKey(1)) - - val original = Message.newInstance( - accounts = listOf(payer), - recentBlockhash = hash, - instructions = emptyList() - ) - - val decoded = Message.newInstance(original.encode().toList()) - assertNotNull(decoded) - assertEquals(hash, decoded.recentBlockhash) - } - - // --- account sorting --- - - @Test - fun signersBeforeNonSigners() { - val payer = AccountMeta.payer(testKey(1)) - val signer = AccountMeta.writable(testKey(2), signer = true) - val nonsigner = AccountMeta.writable(testKey(3)) - - val message = Message.newInstance( - accounts = listOf(nonsigner, signer, payer), - recentBlockhash = testHash(), - instructions = emptyList() - ) - - // First two should be signers - assert(message.accounts[0].isSigner) - assert(message.accounts[1].isSigner) - assert(!message.accounts[2].isSigner) - } - - @Test - fun writableBeforeReadonly() { - val payer = AccountMeta.payer(testKey(1)) - val readonly = AccountMeta.readonly(testKey(2)) - val writable = AccountMeta.writable(testKey(3)) - - val message = Message.newInstance( - accounts = listOf(readonly, writable, payer), - recentBlockhash = testHash(), - instructions = emptyList() - ) - - // Payer first (writable signer), then writable non-signer, then readonly - assert(message.accounts[0].isWritable) - } - - @Test - fun multipleInstructionsRoundtrip() { - val payer = AccountMeta.payer(testKey(1)) - val acc = AccountMeta.writable(testKey(2)) - val program = AccountMeta(testKey(3), isSigner = false, isWritable = false, isPayer = false, isProgram = true) - - val ix1 = Instruction( - program = testKey(3), - accounts = listOf(payer), - data = listOf(0x01) - ) - val ix2 = Instruction( - program = testKey(3), - accounts = listOf(acc), - data = listOf(0x02, 0x03) - ) - - val original = Message.newInstance( - accounts = listOf(payer, acc, program), - recentBlockhash = testHash(), - instructions = listOf(ix1, ix2) - ) - - val decoded = Message.newInstance(original.encode().toList()) - assertNotNull(decoded) - assertEquals(2, decoded.instructions.size) - assertEquals(listOf(0x01), decoded.instructions[0].data) - assertEquals(listOf(0x02, 0x03), decoded.instructions[1].data) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/ProgramAddressTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/ProgramAddressTest.kt deleted file mode 100644 index 39779d706..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/ProgramAddressTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.getcode.solana - -import com.getcode.solana.instructions.programs.AssociatedTokenProgram -import com.getcode.solana.instructions.programs.SystemProgram -import com.getcode.solana.instructions.programs.TokenProgram -import com.getcode.solana.keys.base58 -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class ProgramAddressTest { - - @Test - fun systemProgramAddressIsAllZeros() { - val bytes = SystemProgram.address.bytes - assertEquals(32, bytes.size) - assertTrue(bytes.all { it == 0.toByte() }) - } - - @Test - fun tokenProgramAddressBase58() { - assertEquals( - "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", - TokenProgram.address.base58(), - ) - } - - @Test - fun associatedTokenProgramAddressBase58() { - assertEquals( - "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", - AssociatedTokenProgram.address.base58(), - ) - } - - @Test - fun systemProgramCommandEnumHas12Values() { - assertEquals(12, SystemProgram.Command.entries.size) - } - - @Test - fun systemProgramCommandContainsExpectedValues() { - val commands = SystemProgram.Command.entries.map { it.name } - assertTrue("createAccount" in commands) - assertTrue("assign" in commands) - assertTrue("transfer" in commands) - assertTrue("createAccountWithSeed" in commands) - assertTrue("advanceNonceAccount" in commands) - assertTrue("withdrawNonceAccount" in commands) - assertTrue("initializeNonceAccount" in commands) - assertTrue("authorizeNonceAccount" in commands) - assertTrue("allocate" in commands) - assertTrue("allocateWithSeed" in commands) - assertTrue("assignWithSeed" in commands) - assertTrue("transferWithSeed" in commands) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/ShortVecTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/ShortVecTest.kt deleted file mode 100644 index d1c8e203c..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/ShortVecTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.getcode.solana - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class ShortVecTest { - - @Test - fun encodeLenThenDecodeLenRoundtrip() { - val values = listOf(0, 1, 127, 128, 255, 256, 16383, 16384) - for (value in values) { - val encoded = ShortVec.encodeLen(value) - val (decoded, remaining) = ShortVec.decodeLen(encoded) - assertEquals(value, decoded, "Roundtrip failed for value $value") - assertTrue(remaining.isEmpty(), "No remaining bytes expected for value $value") - } - } - - @Test - fun encodeLenZeroReturnsSingleZeroByte() { - val encoded = ShortVec.encodeLen(0) - assertEquals(listOf(0), encoded) - } - - @Test - fun encodeLenSingleByteNoContinuation() { - val encoded = ShortVec.encodeLen(127) - assertEquals(1, encoded.size) - assertEquals(127.toByte(), encoded[0]) - } - - @Test - fun encodeLenContinuationBitForValue128() { - val encoded = ShortVec.encodeLen(128) - assertEquals(2, encoded.size) - // First byte should have continuation bit set (0x80) - assertTrue((encoded[0].toInt() and 0x80) != 0, "Continuation bit should be set") - // Low 7 bits of first byte = 0, second byte = 1 - assertEquals(0x80.toByte(), encoded[0]) - assertEquals(1.toByte(), encoded[1]) - } - - @Test - fun decodeLenPreservesRemainingBytes() { - val encoded = ShortVec.encodeLen(42) - val extra = listOf(0xAA.toByte(), 0xBB.toByte()) - val input = encoded + extra - val (decoded, remaining) = ShortVec.decodeLen(input) - assertEquals(42, decoded) - assertEquals(extra, remaining) - } - - @Test - fun encodePrependsLengthThenData() { - val data = listOf(10, 20, 30) - val encoded = ShortVec.encode(data) - // First byte(s) should be the length encoding, then the data - val expectedLength = ShortVec.encodeLen(3) - assertEquals(expectedLength + data, encoded) - } - - @Test - fun encodeListEmptyList() { - val encoded = ShortVec.encodeList(emptyList()) - // Should just be the encoded length of 0 - assertEquals(ShortVec.encodeLen(0), encoded) - } - - @Test - fun encodeListSingleItem() { - val item = listOf(1, 2, 3) - val encoded = ShortVec.encodeList(listOf(item)) - val expected = ShortVec.encodeLen(1) + item - assertEquals(expected, encoded) - } - - @Test - fun encodeListMultipleItems() { - val item1 = listOf(1, 2) - val item2 = listOf(3, 4, 5) - val encoded = ShortVec.encodeList(listOf(item1, item2)) - val expected = ShortVec.encodeLen(2) + item1 + item2 - assertEquals(expected, encoded) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/SolanaTransactionTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/SolanaTransactionTest.kt deleted file mode 100644 index e260ab540..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/SolanaTransactionTest.kt +++ /dev/null @@ -1,259 +0,0 @@ -package com.getcode.solana - -import com.getcode.ed25519.Ed25519 -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.Signature -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue -import kotlin.test.assertFailsWith - -class SolanaTransactionTest { - - private fun testHash(): Hash = - Hash(ByteArray(32) { 0xAB.toByte() }.toList()) - - // --- Construction --- - - @Test - fun newInstanceCreatesTransactionWithPlaceholderSignatures() { - val seed = ByteArray(32) { 1 } - val keyPair = Ed25519.createKeyPair(seed) - val payer = PublicKey(keyPair.publicKeyBytes.toList()) - - val tx = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = emptyList() - ) - - assertEquals(1, tx.signatures.size) - assertEquals(Signature.zero, tx.signatures[0]) - } - - @Test - fun newInstanceWithInstructionIncludesProgram() { - val seed = ByteArray(32) { 1 } - val keyPair = Ed25519.createKeyPair(seed) - val payer = PublicKey(keyPair.publicKeyBytes.toList()) - val program = PublicKey(ByteArray(32) { 2 }.toList()) - - val instruction = Instruction( - program = program, - accounts = emptyList(), - data = listOf(0x01) - ) - - val tx = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = listOf(instruction) - ) - - assertTrue(tx.message.accounts.any { it.publicKey == program }) - } - - // --- Signing --- - - @Test - fun signProducesValidSignature() { - val seed = ByteArray(32) { 1 } - val keyPair = Ed25519.createKeyPair(seed) - val payer = PublicKey(keyPair.publicKeyBytes.toList()) - - val tx = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = emptyList() - ) - - val signatures = tx.sign(keyPair) - assertEquals(1, signatures.size) - assertEquals(64, signatures[0].bytes.size) - // Signature should not be all zeros - assertTrue(signatures[0] != Signature.zero) - } - - @Test - fun signatureIsVerifiable() { - val seed = ByteArray(32) { 1 } - val keyPair = Ed25519.createKeyPair(seed) - val payer = PublicKey(keyPair.publicKeyBytes.toList()) - - val tx = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = emptyList() - ) - - val signatures = tx.sign(keyPair) - val messageData = tx.message.encode() - - assertTrue( - Ed25519.verify( - signatures[0].bytes.toByteArray(), - messageData, - keyPair.publicKeyBytes - ) - ) - } - - @Test - fun signIsDeterministic() { - val seed = ByteArray(32) { 1 } - val keyPair = Ed25519.createKeyPair(seed) - val payer = PublicKey(keyPair.publicKeyBytes.toList()) - - val tx1 = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = emptyList() - ) - - val tx2 = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = emptyList() - ) - - val sig1 = tx1.sign(keyPair) - val sig2 = tx2.sign(keyPair) - assertEquals(sig1[0].bytes, sig2[0].bytes) - } - - @Test - fun signRejectsKeyNotInAccountList() { - val seed1 = ByteArray(32) { 1 } - val seed2 = ByteArray(32) { 2 } - val payerKp = Ed25519.createKeyPair(seed1) - val otherKp = Ed25519.createKeyPair(seed2) - val payer = PublicKey(payerKp.publicKeyBytes.toList()) - - val tx = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = emptyList() - ) - - assertFailsWith { - tx.sign(otherKp) - } - } - - @Test - fun signRejectsTooManySigners() { - val seed1 = ByteArray(32) { 1 } - val seed2 = ByteArray(32) { 2 } - val kp1 = Ed25519.createKeyPair(seed1) - val kp2 = Ed25519.createKeyPair(seed2) - val payer = PublicKey(kp1.publicKeyBytes.toList()) - - // Transaction with only 1 required signature (payer) - val tx = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = emptyList() - ) - - assertFailsWith { - tx.sign(kp1, kp2) - } - } - - // --- Encode / Decode roundtrip --- - - @Test - fun encodeDecodeRoundtrip() { - val seed = ByteArray(32) { 1 } - val keyPair = Ed25519.createKeyPair(seed) - val payer = PublicKey(keyPair.publicKeyBytes.toList()) - - val tx = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = emptyList() - ) - - val encoded = tx.encode() - val decoded = SolanaTransaction.fromList(encoded) - - assertNotNull(decoded) - assertEquals(tx.message.header, decoded.message.header) - assertEquals(tx.message.recentBlockhash, decoded.message.recentBlockhash) - assertEquals(tx.signatures.size, decoded.signatures.size) - } - - @Test - fun encodeDecodeWithSignatureRoundtrip() { - val seed = ByteArray(32) { 1 } - val keyPair = Ed25519.createKeyPair(seed) - val payer = PublicKey(keyPair.publicKeyBytes.toList()) - - val tx = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = emptyList() - ) - - val signatures = tx.sign(keyPair) - val signedTx = tx.copy(signatures = signatures) - - val encoded = signedTx.encode() - val decoded = SolanaTransaction.fromList(encoded) - - assertNotNull(decoded) - assertEquals(signatures[0].bytes, decoded.signatures[0].bytes) - } - - @Test - fun encodeDecodeWithInstructionRoundtrip() { - val seed = ByteArray(32) { 1 } - val keyPair = Ed25519.createKeyPair(seed) - val payer = PublicKey(keyPair.publicKeyBytes.toList()) - val program = PublicKey(ByteArray(32) { 3 }.toList()) - - val instruction = Instruction( - program = program, - accounts = listOf(AccountMeta.payer(payer)), - data = listOf(0xCA.toByte(), 0xFE.toByte()) - ) - - val tx = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = listOf(instruction) - ) - - val encoded = tx.encode() - val decoded = SolanaTransaction.fromList(encoded) - - assertNotNull(decoded) - assertEquals(1, decoded.message.instructions.size) - assertEquals( - listOf(0xCA.toByte(), 0xFE.toByte()), - decoded.message.instructions[0].data - ) - } - - // --- Blockhash --- - - @Test - fun recentBlockhashIsSettable() { - val seed = ByteArray(32) { 1 } - val keyPair = Ed25519.createKeyPair(seed) - val payer = PublicKey(keyPair.publicKeyBytes.toList()) - - val tx = SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = testHash(), - instructions = emptyList() - ) - - val newHash = Hash(ByteArray(32) { 0xCD.toByte() }.toList()) - tx.recentBlockhash = newHash - assertEquals(newHash, tx.recentBlockhash) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgramTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgramTest.kt deleted file mode 100644 index 8a46faedc..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/AssociatedTokenProgramTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.PublicKey -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class AssociatedTokenProgramTest { - - private fun key(seed: Byte) = PublicKey(ByteArray(32) { seed }.toList()) - - @Test - fun createAccountRoundtrip() { - val original = AssociatedTokenProgram_CreateAccount( - subsidizer = key(1), owner = key(2), - associatedTokenAccount = key(3), mint = key(4), - ) - val decoded = AssociatedTokenProgram_CreateAccount.newInstance(original.instruction()) - assertEquals(original.subsidizer, decoded.subsidizer) - assertEquals(original.mint, decoded.mint) - } - - @Test - fun createAccountEncodeIsEmpty() { - val inst = AssociatedTokenProgram_CreateAccount(key(1), key(2), key(3), key(4)) - assertTrue(inst.encode().isEmpty()) - } - - @Test - fun createAccountInstructionHasSevenAccounts() { - val inst = AssociatedTokenProgram_CreateAccount(key(1), key(2), key(3), key(4)) - assertEquals(7, inst.instruction().accounts.size) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgramTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgramTest.kt deleted file mode 100644 index b15c27339..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/ComputeBudgetProgramTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.getcode.solana.instructions.programs - -import kotlin.test.Test -import kotlin.test.assertEquals - -class ComputeBudgetProgramTest { - - // --- SetComputeUnitPrice --- - - @Test - fun unitPriceEncodeDecodeRoundtrip() { - val original = ComputeBudgetProgram_SetComputeUnitPrice(microLamports = 50000L, bump = 0) - val instruction = original.instruction() - val decoded = ComputeBudgetProgram_SetComputeUnitPrice.newInstance(instruction) - assertEquals(50000L, decoded.microLamports) - } - - @Test - fun unitPriceZero() { - val original = ComputeBudgetProgram_SetComputeUnitPrice(microLamports = 0L, bump = 0) - val instruction = original.instruction() - val decoded = ComputeBudgetProgram_SetComputeUnitPrice.newInstance(instruction) - assertEquals(0L, decoded.microLamports) - } - - @Test - fun unitPriceLargeValue() { - val original = ComputeBudgetProgram_SetComputeUnitPrice(microLamports = 1_000_000_000L, bump = 0) - val instruction = original.instruction() - val decoded = ComputeBudgetProgram_SetComputeUnitPrice.newInstance(instruction) - assertEquals(1_000_000_000L, decoded.microLamports) - } - - @Test - fun unitPriceEncodeStartsWithCommandByte() { - val price = ComputeBudgetProgram_SetComputeUnitPrice(microLamports = 100, bump = 0) - val encoded = price.encode() - assertEquals(ComputeBudgetProgram.Command.setComputeUnitPrice.ordinal.toByte(), encoded[0]) - } - - @Test - fun unitPriceEncodeHasCorrectLength() { - val price = ComputeBudgetProgram_SetComputeUnitPrice(microLamports = 100, bump = 0) - val encoded = price.encode() - // 1 byte command + 8 bytes Long - assertEquals(9, encoded.size) - } - - // --- SetComputeUnitLimit --- - - @Test - fun unitLimitEncodeDecodeRoundtrip() { - val original = ComputeBudgetProgram_SetComputeUnitLimit(limit = 200_000, bump = 0) - val instruction = original.instruction() - val decoded = ComputeBudgetProgram_SetComputeUnitLimit.newInstance(instruction) - assertEquals(200_000, decoded.limit) - } - - @Test - fun unitLimitZero() { - val original = ComputeBudgetProgram_SetComputeUnitLimit(limit = 0, bump = 0) - val instruction = original.instruction() - val decoded = ComputeBudgetProgram_SetComputeUnitLimit.newInstance(instruction) - assertEquals(0, decoded.limit) - } - - @Test - fun unitLimitMaxValue() { - val original = ComputeBudgetProgram_SetComputeUnitLimit(limit = 1_400_000, bump = 0) - val instruction = original.instruction() - val decoded = ComputeBudgetProgram_SetComputeUnitLimit.newInstance(instruction) - assertEquals(1_400_000, decoded.limit) - } - - @Test - fun unitLimitEncodeStartsWithCommandByte() { - val limit = ComputeBudgetProgram_SetComputeUnitLimit(limit = 100, bump = 0) - val encoded = limit.encode() - assertEquals(ComputeBudgetProgram.Command.setComputeUnitLimit.ordinal.toByte(), encoded[0]) - } - - @Test - fun unitLimitEncodeHasCorrectLength() { - val limit = ComputeBudgetProgram_SetComputeUnitLimit(limit = 100, bump = 0) - val encoded = limit.encode() - // 1 byte command + 4 bytes Int - assertEquals(5, encoded.size) - } - - // --- Command enum --- - - @Test - fun commandValues() { - assertEquals(0.toByte(), ComputeBudgetProgram.Command.requestUnits.value) - assertEquals(1.toByte(), ComputeBudgetProgram.Command.requestHeapFrame.value) - assertEquals(2.toByte(), ComputeBudgetProgram.Command.setComputeUnitLimit.value) - assertEquals(3.toByte(), ComputeBudgetProgram.Command.setComputeUnitPrice.value) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgramTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgramTest.kt deleted file mode 100644 index ee2b30a93..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/SwapValidatorProgramTest.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.keys.PublicKey -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class SwapValidatorProgramTest { - - private fun key(seed: Byte) = PublicKey(ByteArray(32) { seed }.toList()) - - // --- PreSwap --- - - @Test - fun preSwapRoundtrip() { - val remaining = listOf(AccountMeta.readonly(publicKey = key(10))) - val original = SwapValidatorProgram_PreSwap( - preSwapState = key(1), user = key(2), source = key(3), destination = key(4), - nonce = key(5), payer = key(6), remainingAccounts = remaining, - ) - val decoded = SwapValidatorProgram_PreSwap.newInstance(original.instruction()) - assertEquals(original.preSwapState, decoded.preSwapState) - assertEquals(original.user, decoded.user) - assertEquals(original.source, decoded.source) - assertEquals(original.destination, decoded.destination) - assertEquals(original.nonce, decoded.nonce) - assertEquals(original.payer, decoded.payer) - } - - @Test - fun preSwapNoRemainingAccounts() { - val original = SwapValidatorProgram_PreSwap( - preSwapState = key(1), user = key(2), source = key(3), destination = key(4), - nonce = key(5), payer = key(6), remainingAccounts = emptyList(), - ) - val decoded = SwapValidatorProgram_PreSwap.newInstance(original.instruction()) - assertEquals(original.preSwapState, decoded.preSwapState) - } - - @Test - fun preSwapEncodeLength() { - val inst = SwapValidatorProgram_PreSwap( - key(1), key(2), key(3), key(4), key(5), key(6), emptyList(), - ) - // 8 bytes command only (no data fields) - assertEquals(8, inst.encode().size) - } - - // --- PostSwap --- - - @Test - fun postSwapRoundtrip() { - val original = SwapValidatorProgram_PostSwap( - stateBump = 7.toByte(), maxToSend = 5_000_000L, minToReceive = 4_800_000L, - preSwapState = key(1), source = key(2), destination = key(3), payer = key(4), - ) - val decoded = SwapValidatorProgram_PostSwap.newInstance(original.instruction()) - assertEquals(7.toByte(), decoded.stateBump) - assertEquals(5_000_000L, decoded.maxToSend) - assertEquals(4_800_000L, decoded.minToReceive) - assertEquals(original.preSwapState, decoded.preSwapState) - } - - @Test - fun postSwapZeroValues() { - val original = SwapValidatorProgram_PostSwap( - stateBump = 0, maxToSend = 0L, minToReceive = 0L, - preSwapState = key(1), source = key(2), destination = key(3), payer = key(4), - ) - val decoded = SwapValidatorProgram_PostSwap.newInstance(original.instruction()) - assertEquals(0.toByte(), decoded.stateBump) - assertEquals(0L, decoded.maxToSend) - assertEquals(0L, decoded.minToReceive) - } - - @Test - fun postSwapEncodeLength() { - val inst = SwapValidatorProgram_PostSwap( - stateBump = 0, maxToSend = 1L, minToReceive = 1L, - preSwapState = key(1), source = key(2), destination = key(3), payer = key(4), - ) - // 8 bytes command + 1 byte bump + 8 bytes maxToSend + 8 bytes minToReceive - assertEquals(25, inst.encode().size) - } - - // --- Command enum --- - - @Test - fun commandValuesAreDistinct() { - val commands = SwapValidatorProgram.Command.entries - assertEquals(commands.size, commands.map { it.value }.toSet().size) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/SystemProgramTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/SystemProgramTest.kt deleted file mode 100644 index 06740dc0c..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/instructions/programs/SystemProgramTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.solana.keys.PublicKey -import kotlin.test.Test -import kotlin.test.assertEquals - -class SystemProgramTest { - - private fun key(seed: Byte) = PublicKey(ByteArray(32) { seed }.toList()) - - @Test - fun advanceNonceRoundtrip() { - val original = SystemProgram_AdvanceNonce(nonce = key(1), authority = key(2)) - val decoded = SystemProgram_AdvanceNonce.newInstance(original.instruction()) - assertEquals(original.nonce, decoded.nonce) - assertEquals(original.authority, decoded.authority) - } - - @Test - fun advanceNonceEncodeLength() { - val inst = SystemProgram_AdvanceNonce(key(1), key(2)) - // 4 bytes Int command - assertEquals(4, inst.encode().size) - } - - @Test - fun advanceNonceCommandByteValue() { - val inst = SystemProgram_AdvanceNonce(key(1), key(2)) - val encoded = inst.encode() - // advanceNonceAccount ordinal is 4 → LE bytes [4, 0, 0, 0] - assertEquals(4, encoded[0].toInt()) - assertEquals(0, encoded[1].toInt()) - assertEquals(0, encoded[2].toInt()) - assertEquals(0, encoded[3].toInt()) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/keys/AccountMetaTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/solana/keys/AccountMetaTest.kt deleted file mode 100644 index f95f34ec4..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/solana/keys/AccountMetaTest.kt +++ /dev/null @@ -1,188 +0,0 @@ -package com.getcode.solana.keys - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class AccountMetaTest { - - private fun testKey(index: Int): PublicKey { - return PublicKey(ByteArray(32) { index.toByte() }.toList()) - } - - // Factory method tests - - @Test - fun payerSetsCorrectFlags() { - val meta = AccountMeta.payer(testKey(1)) - assertTrue(meta.isSigner) - assertTrue(meta.isWritable) - assertTrue(meta.isPayer) - assertFalse(meta.isProgram) - } - - @Test - fun writableDefaultSetsCorrectFlags() { - val meta = AccountMeta.writable(testKey(2)) - assertFalse(meta.isSigner) - assertTrue(meta.isWritable) - assertFalse(meta.isPayer) - assertFalse(meta.isProgram) - } - - @Test - fun writableWithSignerSetsCorrectFlags() { - val meta = AccountMeta.writable(testKey(2), signer = true) - assertTrue(meta.isSigner) - assertTrue(meta.isWritable) - assertFalse(meta.isPayer) - assertFalse(meta.isProgram) - } - - @Test - fun readonlyDefaultSetsCorrectFlags() { - val meta = AccountMeta.readonly(testKey(3)) - assertFalse(meta.isSigner) - assertFalse(meta.isWritable) - assertFalse(meta.isPayer) - assertFalse(meta.isProgram) - } - - @Test - fun readonlyWithSignerSetsCorrectFlags() { - val meta = AccountMeta.readonly(testKey(3), signer = true) - assertTrue(meta.isSigner) - assertFalse(meta.isWritable) - assertFalse(meta.isPayer) - assertFalse(meta.isProgram) - } - - @Test - fun programSetsCorrectFlags() { - val meta = AccountMeta.program(testKey(4)) - assertFalse(meta.isSigner) - assertFalse(meta.isWritable) - assertFalse(meta.isPayer) - assertTrue(meta.isProgram) - } - - // Sorting tests - - @Test - fun payerSortsBeforeSigner() { - val payer = AccountMeta.payer(testKey(1)) - val signer = AccountMeta.readonly(testKey(2), signer = true) - assertTrue(payer < signer) - } - - @Test - fun signerSortsBeforeWritable() { - val signer = AccountMeta.readonly(testKey(1), signer = true) - val writable = AccountMeta.writable(testKey(2)) - assertTrue(signer < writable) - } - - @Test - fun writableSortsBeforeReadonly() { - val writable = AccountMeta.writable(testKey(1)) - val readonly = AccountMeta.readonly(testKey(2)) - assertTrue(writable < readonly) - } - - @Test - fun readonlySortsBeforeProgram() { - val readonly = AccountMeta.readonly(testKey(1)) - val program = AccountMeta.program(testKey(2)) - assertTrue(readonly < program) - } - - @Test - fun fullSortOrder() { - val program = AccountMeta.program(testKey(5)) - val readonly = AccountMeta.readonly(testKey(4)) - val writable = AccountMeta.writable(testKey(3)) - val signer = AccountMeta.readonly(testKey(2), signer = true) - val payer = AccountMeta.payer(testKey(1)) - - val sorted = listOf(program, readonly, writable, signer, payer).sorted() - - assertEquals(payer, sorted[0]) - assertEquals(signer, sorted[1]) - assertEquals(writable, sorted[2]) - assertEquals(readonly, sorted[3]) - assertEquals(program, sorted[4]) - } - - // filterUniqueAccounts tests - - @Test - fun filterUniqueAccountsDeduplicates() { - val key = testKey(1) - val meta1 = AccountMeta.readonly(key) - val meta2 = AccountMeta.readonly(key) - val result = listOf(meta1, meta2).filterUniqueAccounts() - assertEquals(1, result.size) - } - - @Test - fun filterUniqueAccountsPromotesPermissions() { - val key = testKey(1) - val readonly = AccountMeta.readonly(key) - val writable = AccountMeta.writable(key) - val result = listOf(readonly, writable).filterUniqueAccounts() - - assertEquals(1, result.size) - assertTrue(result[0].isWritable, "Should promote to writable") - } - - @Test - fun filterUniqueAccountsPromotesSigner() { - val key = testKey(1) - val nonSigner = AccountMeta.writable(key, signer = false) - val signer = AccountMeta.readonly(key, signer = true) - val result = listOf(nonSigner, signer).filterUniqueAccounts() - - assertEquals(1, result.size) - assertTrue(result[0].isSigner, "Should promote to signer") - assertTrue(result[0].isWritable, "Should retain writable from first entry") - } - - @Test - fun filterUniqueAccountsPreservesDistinctKeys() { - val meta1 = AccountMeta.readonly(testKey(1)) - val meta2 = AccountMeta.writable(testKey(2)) - val result = listOf(meta1, meta2).filterUniqueAccounts() - assertEquals(2, result.size) - } - - // compareLexicographically tests - - @Test - fun compareLexicographicallyEqual() { - val a = byteArrayOf(1, 2, 3) - val b = byteArrayOf(1, 2, 3) - assertEquals(0, AccountMeta.compareLexicographically(a, b)) - } - - @Test - fun compareLexicographicallyLessThan() { - val a = byteArrayOf(1, 2, 3) - val b = byteArrayOf(1, 2, 4) - assertTrue(AccountMeta.compareLexicographically(a, b) < 0) - } - - @Test - fun compareLexicographicallyGreaterThan() { - val a = byteArrayOf(1, 3, 3) - val b = byteArrayOf(1, 2, 3) - assertTrue(AccountMeta.compareLexicographically(a, b) > 0) - } - - @Test - fun compareLexicographicallyShorterArray() { - val a = byteArrayOf(1, 2) - val b = byteArrayOf(1, 2, 3) - assertTrue(AccountMeta.compareLexicographically(a, b) < 0) - } -} diff --git a/libs/crypto/solana/src/test/kotlin/com/getcode/utils/DataSliceTest.kt b/libs/crypto/solana/src/test/kotlin/com/getcode/utils/DataSliceTest.kt deleted file mode 100644 index 9dfbf5dcd..000000000 --- a/libs/crypto/solana/src/test/kotlin/com/getcode/utils/DataSliceTest.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.getcode.utils - -import com.getcode.utils.DataSlice.byteToUnsignedInt -import com.getcode.utils.DataSlice.canConsume -import com.getcode.utils.DataSlice.chunk -import com.getcode.utils.DataSlice.consume -import com.getcode.utils.DataSlice.prefix -import com.getcode.utils.DataSlice.suffix -import com.getcode.utils.DataSlice.tail -import com.getcode.utils.DataSlice.toLong -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class DataSliceTest { - - // --- canConsume --- - - @Test - fun canConsumeTrue() { - assertTrue(listOf(1, 2, 3).canConsume(3)) - } - - @Test - fun canConsumeFalse() { - assertFalse(listOf(1, 2).canConsume(3)) - } - - @Test - fun canConsumeZero() { - assertTrue(emptyList().canConsume(0)) - } - - // --- consume --- - - @Test - fun consumeSplitsCorrectly() { - val result = listOf(1, 2, 3, 4, 5).consume(3) - assertEquals(listOf(1, 2, 3), result.consumed) - assertEquals(listOf(4, 5), result.remaining) - } - - @Test - fun consumeZeroReturnsEmptyConsumed() { - val result = listOf(1, 2).consume(0) - assertEquals(emptyList(), result.consumed) - assertEquals(listOf(1, 2), result.remaining) - } - - @Test - fun consumeAll() { - val result = listOf(1, 2, 3).consume(3) - assertEquals(listOf(1, 2, 3), result.consumed) - assertEquals(emptyList(), result.remaining) - } - - // --- prefix --- - - @Test - fun prefixReturnsFirstN() { - assertEquals(listOf(1, 2), listOf(1, 2, 3, 4).prefix(2)) - } - - @Test - fun prefixOutOfBoundsReturnsEmpty() { - assertEquals(emptyList(), listOf(1, 2).prefix(5)) - } - - @Test - fun prefixNegativeReturnsEmpty() { - assertEquals(emptyList(), listOf(1, 2).prefix(-1)) - } - - // --- suffix --- - - @Test - fun suffixReturnsFromIndex() { - assertEquals(listOf(3, 4), listOf(1, 2, 3, 4).suffix(2)) - } - - @Test - fun suffixOutOfBoundsReturnsEmpty() { - assertEquals(emptyList(), listOf(1, 2).suffix(5)) - } - - // --- tail --- - - @Test - fun tailIsSameAsSuffix() { - val data = listOf(1, 2, 3, 4, 5) - assertEquals(data.suffix(2), data.tail(2)) - } - - // --- chunk --- - - @Test - fun chunkSplitsIntoEqualParts() { - val data = listOf(1, 2, 3, 4, 5, 6) - val chunks = data.chunk(2, 3) { it.toList() } - assertEquals(3, chunks?.size) - assertEquals(listOf(1, 2), chunks?.get(0)) - assertEquals(listOf(3, 4), chunks?.get(1)) - assertEquals(listOf(5, 6), chunks?.get(2)) - } - - @Test - fun chunkTooLargeReturnsNull() { - assertNull(listOf(1, 2).chunk(2, 3) { it }) - } - - // --- byteToUnsignedInt --- - - @Test - fun positiveByteUnchanged() { - assertEquals(127, (127).toByte().byteToUnsignedInt()) - } - - @Test - fun negativeByteConverted() { - assertEquals(128, (-128).toByte().byteToUnsignedInt()) - assertEquals(255, (-1).toByte().byteToUnsignedInt()) - } - - @Test - fun zeroByteUnchanged() { - assertEquals(0, (0).toByte().byteToUnsignedInt()) - } - - // --- ByteArray.toLong --- - - @Test - fun byteArrayToLongSingleByte() { - assertEquals(42L, byteArrayOf(42).toLong()) - } - - @Test - fun byteArrayToLongLittleEndian() { - // 0x0100 in little-endian = 256 - assertEquals(256L, byteArrayOf(0, 1).toLong()) - } - - @Test - fun byteArrayToLongAllZeros() { - assertEquals(0L, byteArrayOf(0, 0, 0, 0).toLong()) - } - - @Test - fun byteArrayToLongMaxUnsignedByte() { - assertEquals(255L, byteArrayOf(-1).toLong()) - } -}