From ce8be3d721c94733c1665d53f9d8dc6c59b98655 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 10 Sep 2026 15:10:23 -0400 Subject: [PATCH] Revert "fix(oc): match the Coinbase stable swapper upgrade (#1439)" This reverts commit 7f75c0d73ab58545ab293e322d78f255bbbd8243. --- .../internal/solana/extensions/PublicKey.kt | 7 ++ .../model/CoinbaseStablecoinPoolAccount.kt | 47 ++---------- .../solana/model/CoinbaseSwapAccounts.kt | 4 + .../CoinbaseStableSwapperProgram_Swap.kt | 2 + .../CoinbaseStablecoinSwapperInstructions.kt | 1 + .../swap/UsdcDepositSweepInstructions.kt | 1 + .../solana/swap/UsdcToUsdfSwapInstructions.kt | 1 + .../solana/swap/UsdfDepositInstructions.kt | 1 + .../solana/extensions/PdaDerivationTest.kt | 23 +++++- .../CoinbaseStablecoinPoolAccountTest.kt | 73 ------------------- .../CoinbaseStableSwapperProgramTest.kt | 31 ++++---- ...inbaseStablecoinSwapperInstructionsTest.kt | 8 ++ .../swap/StatelessSwapInstructionsTest.kt | 8 ++ 13 files changed, 76 insertions(+), 131 deletions(-) delete mode 100644 services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseStablecoinPoolAccountTest.kt diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/extensions/PublicKey.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/extensions/PublicKey.kt index 766c543ae1..a17f9d2460 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/extensions/PublicKey.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/extensions/PublicKey.kt @@ -172,6 +172,13 @@ internal fun PublicKey.Companion.deriveCoinbaseVaultTokenAccountAddress(vault: P ) } +internal fun PublicKey.Companion.deriveCoinbaseWhitelistAddress(): ProgramDerivedAccount { + return findProgramAddress( + seeds = listOf("address_whitelist".toByteArray(Charsets.UTF_8)), + programId = CoinbaseStableSwapperProgram.address, + ) +} + /// FindProgramAddress mirrors the implementation of the Solana SDK's FindProgramAddress. Its primary /// use case (for Kin and Agora) is for deriving associated accounts. /// diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseStablecoinPoolAccount.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseStablecoinPoolAccount.kt index 51c2fa418c..a23a5e1141 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseStablecoinPoolAccount.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseStablecoinPoolAccount.kt @@ -5,56 +5,23 @@ import com.getcode.solana.keys.PublicKey /** * Represents the on-chain CoinbaseStableSwapper liquidity pool account. * - * Mirrors `LiquidityPool` in coinbase/stable-swapper (`programs/stable-swapper/src/state.rs`) - * after the role-based authority migration (coinbase/stable-swapper#20). Layout: - * - * ``` - * [8 discriminator] - * [32 pause_authority][32 unpause_authority][32 treasury_authority][32 configure_authority] - * [32 fee_recipient] - * [4 + 32n withdraw_recipients][4 + 32n supported_tokens] - * [8 fee_rate][1 swaps_paused][1 liquidity_paused][1 bump] - * ``` - * - * Only the fixed-offset prefix is parsed; the client needs the fee recipient and nothing - * after the vectors. + * Layout: + * [8 discriminator][32 operations_authority][32 pause_authority][32 fee_recipient]... */ internal data class CoinbaseStablecoinPoolAccount( - val pauseAuthority: PublicKey, - val unpauseAuthority: PublicKey, - val treasuryAuthority: PublicKey, - val configureAuthority: PublicKey, val feeRecipient: PublicKey, ) { companion object { - // LiquidityPool discriminator: [66, 38, 17, 64, 188, 80, 68, 129] - private val DISCRIMINATOR = byteArrayOf(66, 38, 17, 64, 188.toByte(), 80, 68, 129.toByte()) - - private const val KEY_SIZE = 32 - private const val PAUSE_AUTHORITY_OFFSET = 8 - private const val UNPAUSE_AUTHORITY_OFFSET = PAUSE_AUTHORITY_OFFSET + KEY_SIZE - private const val TREASURY_AUTHORITY_OFFSET = UNPAUSE_AUTHORITY_OFFSET + KEY_SIZE - private const val CONFIGURE_AUTHORITY_OFFSET = TREASURY_AUTHORITY_OFFSET + KEY_SIZE - private const val FEE_RECIPIENT_OFFSET = CONFIGURE_AUTHORITY_OFFSET + KEY_SIZE - private const val FIXED_PREFIX_SIZE = FEE_RECIPIENT_OFFSET + KEY_SIZE + private const val FEE_RECIPIENT_OFFSET = 8 + 32 + 32 // discriminator + ops_authority + pause_authority fun fromAccountData(data: ByteArray): CoinbaseStablecoinPoolAccount { - require(data.size >= FIXED_PREFIX_SIZE) { - "Account data too short: expected at least $FIXED_PREFIX_SIZE bytes, got ${data.size}" - } - require(data.sliceArray(0 until DISCRIMINATOR.size).contentEquals(DISCRIMINATOR)) { - "Account data is not a CoinbaseStableSwapper LiquidityPool (discriminator mismatch)" + require(data.size >= FEE_RECIPIENT_OFFSET + 32) { + "Account data too short: expected at least ${FEE_RECIPIENT_OFFSET + 32} bytes, got ${data.size}" } + val feeRecipientBytes = data.sliceArray(FEE_RECIPIENT_OFFSET until FEE_RECIPIENT_OFFSET + 32) return CoinbaseStablecoinPoolAccount( - pauseAuthority = data.keyAt(PAUSE_AUTHORITY_OFFSET), - unpauseAuthority = data.keyAt(UNPAUSE_AUTHORITY_OFFSET), - treasuryAuthority = data.keyAt(TREASURY_AUTHORITY_OFFSET), - configureAuthority = data.keyAt(CONFIGURE_AUTHORITY_OFFSET), - feeRecipient = data.keyAt(FEE_RECIPIENT_OFFSET), + feeRecipient = PublicKey(feeRecipientBytes.toList()), ) } - - private fun ByteArray.keyAt(offset: Int): PublicKey = - PublicKey(sliceArray(offset until offset + KEY_SIZE).toList()) } } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseSwapAccounts.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseSwapAccounts.kt index 9f6ddc6f43..3c9fdeb3d9 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseSwapAccounts.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseSwapAccounts.kt @@ -4,6 +4,7 @@ import com.getcode.opencode.internal.solana.extensions.deriveAssociatedAccount import com.getcode.opencode.internal.solana.extensions.deriveCoinbasePoolAddress import com.getcode.opencode.internal.solana.extensions.deriveCoinbaseTokenVaultAddress import com.getcode.opencode.internal.solana.extensions.deriveCoinbaseVaultTokenAccountAddress +import com.getcode.opencode.internal.solana.extensions.deriveCoinbaseWhitelistAddress import com.getcode.solana.keys.PublicKey internal data class CoinbaseSwapAccounts( @@ -12,6 +13,7 @@ internal data class CoinbaseSwapAccounts( val outVault: PublicKey, val inVaultTokenAccount: PublicKey, val outVaultTokenAccount: PublicKey, + val whitelist: PublicKey, ) { fun feeRecipientTokenAccount(feeRecipient: PublicKey, fromMint: PublicKey): PublicKey { return PublicKey.deriveAssociatedAccount( @@ -27,6 +29,7 @@ internal data class CoinbaseSwapAccounts( val outVault = PublicKey.deriveCoinbaseTokenVaultAddress(pool, toMint).publicKey val inVaultTokenAccount = PublicKey.deriveCoinbaseVaultTokenAccountAddress(inVault).publicKey val outVaultTokenAccount = PublicKey.deriveCoinbaseVaultTokenAccountAddress(outVault).publicKey + val whitelist = PublicKey.deriveCoinbaseWhitelistAddress().publicKey return CoinbaseSwapAccounts( pool = pool, @@ -34,6 +37,7 @@ internal data class CoinbaseSwapAccounts( outVault = outVault, inVaultTokenAccount = inVaultTokenAccount, outVaultTokenAccount = outVaultTokenAccount, + whitelist = whitelist, ) } } diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgram_Swap.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgram_Swap.kt index 297c30bbb6..c156d67ed9 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgram_Swap.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgram_Swap.kt @@ -18,6 +18,7 @@ internal class CoinbaseStableSwapperProgram_Swap( private val fromMint: PublicKey, private val toMint: PublicKey, private val user: PublicKey, + private val whitelist: PublicKey, private val amountIn: Long, private val minAmountOut: Long, ) : InstructionType { @@ -37,6 +38,7 @@ internal class CoinbaseStableSwapperProgram_Swap( AccountMeta.readonly(publicKey = fromMint), AccountMeta.readonly(publicKey = toMint), AccountMeta.writable(publicKey = user, signer = true), + AccountMeta.readonly(publicKey = whitelist), AccountMeta.readonly(publicKey = TokenProgram.address), AccountMeta.readonly(publicKey = AssociatedTokenProgram.address), AccountMeta.readonly(publicKey = SystemProgram.address), diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructions.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructions.kt index ad682aca9b..3d78a61738 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructions.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructions.kt @@ -130,6 +130,7 @@ internal fun buildStablecoinSwapperInstructions( fromMint = fromMintMetadata.address, toMint = toMintMetadata.address, user = swapAuthority, + whitelist = swapAccounts.whitelist, amountIn = amount, minAmountOut = minOutput, ).instruction() diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdcDepositSweepInstructions.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdcDepositSweepInstructions.kt index c4610ba9bf..6dcff213eb 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdcDepositSweepInstructions.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdcDepositSweepInstructions.kt @@ -111,6 +111,7 @@ internal fun buildStatelessSwapInstructions( fromMint = fromMint.address, toMint = toMint.address, user = owner, + whitelist = swapAccounts.whitelist, amountIn = amount, minAmountOut = amount, // 1:1 stable pair ).instruction() diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdcToUsdfSwapInstructions.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdcToUsdfSwapInstructions.kt index 5d4f74476c..62243ecd1a 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdcToUsdfSwapInstructions.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdcToUsdfSwapInstructions.kt @@ -103,6 +103,7 @@ internal fun buildUsdcToUsdfSwapInstructions( fromMint = Mint.usdc, toMint = Mint.usdf, user = sender, + whitelist = swapAccounts.whitelist, amountIn = amount, minAmountOut = 0, ).instruction() diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdfDepositInstructions.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdfDepositInstructions.kt index 5767eb0a81..0fd537cd85 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdfDepositInstructions.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/swap/UsdfDepositInstructions.kt @@ -97,6 +97,7 @@ internal fun buildUsdfDepositInstructions( fromMint = Mint.usdc, toMint = Mint.usdf, user = sender, + whitelist = swapAccounts.whitelist, amountIn = amount, minAmountOut = amount, // 1:1 stable pair ).instruction() diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/extensions/PdaDerivationTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/extensions/PdaDerivationTest.kt index c199c269c9..02b327010f 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/extensions/PdaDerivationTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/extensions/PdaDerivationTest.kt @@ -247,6 +247,22 @@ class PdaDerivationTest { assertNotEquals(result1.publicKey, result2.publicKey) } + // --- deriveCoinbaseWhitelistAddress --- + + @Test + fun coinbaseWhitelistIsDeterministic() { + val result1 = PublicKey.deriveCoinbaseWhitelistAddress() + val result2 = PublicKey.deriveCoinbaseWhitelistAddress() + assertEquals(result1.publicKey, result2.publicKey) + assertEquals(result1.bump, result2.bump) + } + + @Test + fun coinbaseWhitelistIsOffCurve() { + val result = PublicKey.deriveCoinbaseWhitelistAddress() + assertTrue(!Ed25519.onCurve(result.publicKey.bytes.toByteArray())) + } + // --- Coinbase PDA chain: pool -> vault -> vaultTokenAccount --- @Test @@ -255,10 +271,11 @@ class PdaDerivationTest { val mint = testKey(10) val vault = PublicKey.deriveCoinbaseTokenVaultAddress(pool, mint).publicKey val vaultTA = PublicKey.deriveCoinbaseVaultTokenAccountAddress(vault).publicKey + val whitelist = PublicKey.deriveCoinbaseWhitelistAddress().publicKey - // All three should be distinct - val all = setOf(pool, vault, vaultTA) - assertEquals(3, all.size, "pool, vault, and vaultTA should all be distinct") + // All four should be distinct + val all = setOf(pool, vault, vaultTA, whitelist) + assertEquals(4, all.size, "pool, vault, vaultTA, and whitelist should all be distinct") } // --- Known value: well-known associated token address --- diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseStablecoinPoolAccountTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseStablecoinPoolAccountTest.kt deleted file mode 100644 index 822bab7678..0000000000 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseStablecoinPoolAccountTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.getcode.opencode.internal.solana.model - -import com.getcode.solana.keys.base58 -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -@OptIn(ExperimentalEncodingApi::class) -class CoinbaseStablecoinPoolAccountTest { - - // First 315 bytes of mainnet pool CrDL9SoCyW1tBgn8k7rgGSpWhnszneWDbvKvqPAU4PL9 - // at slot ~445672210, after the 2026-09-08 MigrateAuthorities instruction. The - // live account is 2107 bytes with the remainder zeroed. Same fixture as ocp-server. - private val mainnetPrefix = "QiYRQLxQRIEFHqE9vluQFKO1wbEwnd22aRe9qGrV03SIsz1AV7GqT/yEzcR/f+ALaKG4KMxbBfZ5dTNFPqxxZHMfbTqNfj6St0p+" + - "+yObz2IILMcKsoko07O+oRSDek7YwzrH9TroL1+QbHdT/t9qpcq4Kyx8OPWZm79AIUM9UlN+X6ujF0hPgzT4z8SXtrVTfBhZj7Lz" + - "SPTgCpHoi6cjfPqXvflOJvRBAgAAAN0H70q0C5DeChX575Umuo4KwnYx+lqZmPTGnq1wMK2QSoyv1lJlvQkMgeq0VkN3NML2MHaz" + - "cTzSusODf5c6RhACAAAAxvp6877brTo9ZfNqq8l0MbG75MLS9uDkfKYCA0UvXWE908SAij1Ps5+uycukm1pjSlLt4RVI9SSqIPLN" + - "Ru+AcQAAAAAAAAAAAAD/" - - private fun mainnetData(): ByteArray { - val prefix = Base64.decode(mainnetPrefix) - assertEquals(315, prefix.size) - return prefix.copyOf(2107) - } - - @Test - fun `parses mainnet pool account after authority migration`() { - val pool = CoinbaseStablecoinPoolAccount.fromAccountData(mainnetData()) - - assertEquals("Lz8QXHjETKQnt1fzsKbN4AyQEhhVAFB2YKwAMqksr7G", pool.pauseAuthority.base58()) - assertEquals("HzjC9U1WifkqLhYMx592UG1fJqX3BtRjCqPPPTo3hA8R", pool.unpauseAuthority.base58()) - assertEquals("DLVVcd3xfwWeCwGz1EUQbqaNC88NooN6s9ifWo87QZst", pool.treasuryAuthority.base58()) - assertEquals("Aimdv5hcHfm2PKuGwDW9H81iibZoYKLv3TPMEaZhmvqG", pool.configureAuthority.base58()) - assertEquals("4ZnFXk7KyB5khDqjWSHqHBQH1nQCnmvkr1pRFivWcP7e", pool.feeRecipient.base58()) - } - - @Test - fun `fee recipient is read at the post-migration offset`() { - // Pre-migration layout put fee_recipient at byte 72; reading there now yields - // treasury_authority, which would send pool fees to the wrong account. - val pool = CoinbaseStablecoinPoolAccount.fromAccountData(mainnetData()) - assertEquals(pool.treasuryAuthority, keyAt(mainnetData(), 72)) - assertEquals(pool.feeRecipient, keyAt(mainnetData(), 136)) - } - - @Test - fun `rejects truncated data`() { - val data = mainnetData() - for (size in listOf(0, 8, 135, 167)) { - assertFailsWith("size $size") { - CoinbaseStablecoinPoolAccount.fromAccountData(data.copyOf(size)) - } - } - // Exactly the fixed prefix is enough. - CoinbaseStablecoinPoolAccount.fromAccountData(data.copyOf(168)) - } - - @Test - fun `rejects wrong discriminator`() { - val data = mainnetData() - // TokenVault discriminator: [121, 7, 84, 254, 151, 228, 43, 144] - byteArrayOf(121, 7, 84, 254.toByte(), 151.toByte(), 228.toByte(), 43, 144.toByte()) - .copyInto(data) - assertFailsWith { - CoinbaseStablecoinPoolAccount.fromAccountData(data) - } - } - - private fun keyAt(data: ByteArray, offset: Int) = - com.getcode.solana.keys.PublicKey(data.sliceArray(offset until offset + 32).toList()) -} diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgramTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgramTest.kt index 8dd33e0817..92bbfff95f 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgramTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgramTest.kt @@ -32,7 +32,7 @@ class CoinbaseStableSwapperProgramTest { userFromTokenAccount = testKey(6), toTokenAccount = testKey(7), feeRecipientTokenAccount = testKey(8), feeRecipient = testKey(9), fromMint = testKey(10), toMint = testKey(11), - user = testKey(12), + user = testKey(12), whitelist = testKey(13), amountIn = 0L, minAmountOut = 0L, ) val encoded = ix.encode() @@ -57,7 +57,7 @@ class CoinbaseStableSwapperProgramTest { userFromTokenAccount = testKey(6), toTokenAccount = testKey(7), feeRecipientTokenAccount = testKey(8), feeRecipient = testKey(9), fromMint = testKey(10), toMint = testKey(11), - user = testKey(12), + user = testKey(12), whitelist = testKey(13), amountIn = 1000L, minAmountOut = 900L, ) // 8 bytes discriminator + 8 bytes amountIn + 8 bytes minAmountOut = 24 @@ -72,7 +72,7 @@ class CoinbaseStableSwapperProgramTest { userFromTokenAccount = testKey(6), toTokenAccount = testKey(7), feeRecipientTokenAccount = testKey(8), feeRecipient = testKey(9), fromMint = testKey(10), toMint = testKey(11), - user = testKey(12), + user = testKey(12), whitelist = testKey(13), amountIn = 1_000_000L, minAmountOut = 999_000L, ) val encoded = ix.encode() @@ -93,17 +93,17 @@ class CoinbaseStableSwapperProgramTest { // --- Swap instruction accounts --- @Test - fun swapInstructionHas15Accounts() { + fun swapInstructionHas16Accounts() { val ix = CoinbaseStableSwapperProgram_Swap( pool = testKey(1), inVault = testKey(2), outVault = testKey(3), inVaultTokenAccount = testKey(4), outVaultTokenAccount = testKey(5), userFromTokenAccount = testKey(6), toTokenAccount = testKey(7), feeRecipientTokenAccount = testKey(8), feeRecipient = testKey(9), fromMint = testKey(10), toMint = testKey(11), - user = testKey(12), + user = testKey(12), whitelist = testKey(13), amountIn = 1000L, minAmountOut = 900L, ) - assertEquals(15, ix.instruction().accounts.size) + assertEquals(16, ix.instruction().accounts.size) } @Test @@ -114,7 +114,7 @@ class CoinbaseStableSwapperProgramTest { userFromTokenAccount = testKey(6), toTokenAccount = testKey(7), feeRecipientTokenAccount = testKey(8), feeRecipient = testKey(9), fromMint = testKey(10), toMint = testKey(11), - user = testKey(12), + user = testKey(12), whitelist = testKey(13), amountIn = 1000L, minAmountOut = 900L, ) assertEquals(CoinbaseStableSwapperProgram.address, ix.instruction().program) @@ -122,20 +122,20 @@ class CoinbaseStableSwapperProgramTest { @Test fun swapAccountOrder() { - val keys = (1..12).map { testKey(it) } + val keys = (1..13).map { testKey(it) } val ix = CoinbaseStableSwapperProgram_Swap( pool = keys[0], inVault = keys[1], outVault = keys[2], inVaultTokenAccount = keys[3], outVaultTokenAccount = keys[4], userFromTokenAccount = keys[5], toTokenAccount = keys[6], feeRecipientTokenAccount = keys[7], feeRecipient = keys[8], fromMint = keys[9], toMint = keys[10], - user = keys[11], + user = keys[11], whitelist = keys[12], amountIn = 1000L, minAmountOut = 900L, ) val accounts = ix.instruction().accounts // Verify account order matches server: pool, inVault, outVault, inVaultTA, outVaultTA, - // userFromTA, toTA, feeRecipientTA, feeRecipient, fromMint, toMint, user, + // userFromTA, toTA, feeRecipientTA, feeRecipient, fromMint, toMint, user, whitelist, // tokenProgram, associatedTokenProgram, systemProgram assertEquals(keys[0], accounts[0].publicKey) // pool assertEquals(keys[1], accounts[1].publicKey) // inVault @@ -149,9 +149,10 @@ class CoinbaseStableSwapperProgramTest { assertEquals(keys[9], accounts[9].publicKey) // fromMint assertEquals(keys[10], accounts[10].publicKey) // toMint assertEquals(keys[11], accounts[11].publicKey) // user - assertEquals(TokenProgram.address, accounts[12].publicKey) - assertEquals(AssociatedTokenProgram.address, accounts[13].publicKey) - assertEquals(SystemProgram.address, accounts[14].publicKey) + assertEquals(keys[12], accounts[12].publicKey) // whitelist + assertEquals(TokenProgram.address, accounts[13].publicKey) + assertEquals(AssociatedTokenProgram.address, accounts[14].publicKey) + assertEquals(SystemProgram.address, accounts[15].publicKey) } @Test @@ -162,7 +163,7 @@ class CoinbaseStableSwapperProgramTest { userFromTokenAccount = testKey(6), toTokenAccount = testKey(7), feeRecipientTokenAccount = testKey(8), feeRecipient = testKey(9), fromMint = testKey(10), toMint = testKey(11), - user = testKey(12), + user = testKey(12), whitelist = testKey(13), amountIn = 1000L, minAmountOut = 900L, ) val accounts = ix.instruction().accounts @@ -185,7 +186,7 @@ class CoinbaseStableSwapperProgramTest { userFromTokenAccount = testKey(6), toTokenAccount = testKey(7), feeRecipientTokenAccount = testKey(8), feeRecipient = testKey(9), fromMint = testKey(10), toMint = testKey(11), - user = testKey(12), + user = testKey(12), whitelist = testKey(13), amountIn = 1000L, minAmountOut = 900L, ) val accounts = ix.instruction().accounts diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructionsTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructionsTest.kt index 4ac2e038f9..00896e1d95 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructionsTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinSwapperInstructionsTest.kt @@ -3,6 +3,7 @@ package com.getcode.opencode.solana.swap import com.getcode.opencode.internal.solana.extensions.deriveCoinbasePoolAddress import com.getcode.opencode.internal.solana.extensions.deriveCoinbaseTokenVaultAddress import com.getcode.opencode.internal.solana.extensions.deriveCoinbaseVaultTokenAccountAddress +import com.getcode.opencode.internal.solana.extensions.deriveCoinbaseWhitelistAddress import com.getcode.opencode.internal.solana.extensions.deriveAssociatedAccount import com.getcode.opencode.internal.solana.programs.AssociatedTokenProgram import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram @@ -253,6 +254,13 @@ class CoinbaseStablecoinSwapperInstructionsTest { assertTrue(ix.accounts[11].isSigner) } + @Test + fun `swap instruction has correct whitelist PDA`() { + val ix = buildInstructions()[7] + val expectedWhitelist = PublicKey.deriveCoinbaseWhitelistAddress().publicKey + assertEquals(expectedWhitelist, ix.accounts[12].publicKey) + } + // --- CloseAccount verification --- @Test diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/StatelessSwapInstructionsTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/StatelessSwapInstructionsTest.kt index 8acbfe2985..37cb7acfee 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/StatelessSwapInstructionsTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/StatelessSwapInstructionsTest.kt @@ -4,6 +4,7 @@ import com.getcode.opencode.internal.solana.extensions.deriveAssociatedAccount import com.getcode.opencode.internal.solana.extensions.deriveCoinbasePoolAddress import com.getcode.opencode.internal.solana.extensions.deriveCoinbaseTokenVaultAddress import com.getcode.opencode.internal.solana.extensions.deriveCoinbaseVaultTokenAccountAddress +import com.getcode.opencode.internal.solana.extensions.deriveCoinbaseWhitelistAddress import com.getcode.opencode.internal.solana.extensions.deriveDepositAccount import com.getcode.opencode.internal.solana.extensions.deriveVirtualMachineAccount import com.getcode.opencode.internal.solana.programs.AssociatedTokenProgram @@ -295,6 +296,13 @@ class StatelessSwapInstructionsTest { assertTrue(ix.accounts[11].isSigner) } + @Test + fun `swap instruction has correct whitelist PDA`() { + val ix = buildInstructions()[4] + val expectedWhitelist = PublicKey.deriveCoinbaseWhitelistAddress().publicKey + assertEquals(expectedWhitelist, ix.accounts[12].publicKey) + } + // --- CreateIdempotent and Swap destination linkage --- @Test