diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fef8a2bd1a..ff83cf456c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,7 +68,7 @@ protovalidate-kt = "0.1.3" # cadence and there is nothing to keep aligned. # 0.3.0 is the first release of either package to ship R8 keep rules for its generated # messages, which is what lets proguard-rules.pro drop its own. -ocp-client-protocol = "0.8.0" +ocp-client-protocol = "0.9.0" flipcash2-client-protocol = "0.20.0" # The Android port is the ONLY libphonenumber this app depends on, deliberately. Google's diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentStatelessSwap.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentStatelessSwap.kt index f4219e2fd2..5d0d80d156 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentStatelessSwap.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/api/intents/IntentStatelessSwap.kt @@ -4,6 +4,7 @@ import com.codeinc.opencode.gen.transaction.v1.OcpTransactionService import com.getcode.opencode.internal.network.extensions.asSignature import com.getcode.opencode.internal.network.extensions.asSolanaAccountId import com.getcode.opencode.internal.network.extensions.sign +import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram import com.getcode.opencode.model.financial.MintMetadata import com.getcode.opencode.model.financial.usdc import com.getcode.opencode.model.financial.usdf @@ -33,17 +34,20 @@ internal class IntentStatelessSwap( ) } + internal fun stablecoinParams(): OcpTransactionService.StatelessSwapRequest.Initiate.CoinbaseStableSwapperClientParameters = + OcpTransactionService.StatelessSwapRequest.Initiate.CoinbaseStableSwapperClientParameters.newBuilder() + .setFromMint(request.fromMint.asSolanaAccountId()) + .setToMint(request.toMint.asSolanaAccountId()) + .setSwapAmount(request.amount) + // Same constant the transaction builder defaults to, so request and local build agree. + .setSwapVersion(CoinbaseStableSwapperProgram.SERVER_SWAP_VERSION) + .build() + fun initiate(): OcpTransactionService.StatelessSwapRequest { val owner = request.owner.authority.keyPair val initiate = OcpTransactionService.StatelessSwapRequest.Initiate.newBuilder() .setOwner(owner.asSolanaAccountId()) - .setStablecoin( - OcpTransactionService.StatelessSwapRequest.Initiate.CoinbaseStableSwapperClientParameters.newBuilder() - .setFromMint(request.fromMint.asSolanaAccountId()) - .setToMint(request.toMint.asSolanaAccountId()) - .setSwapAmount(request.amount) - .build() - ) + .setStablecoin(stablecoinParams()) .setWaitForFinalization(request.waitForFinalization) .apply { setSignature(sign(owner)) } .build() diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/LocalToProtobuf.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/LocalToProtobuf.kt index e737a1a9e5..377ea7a5ff 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/LocalToProtobuf.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/network/extensions/LocalToProtobuf.kt @@ -9,6 +9,7 @@ import com.codeinc.opencode.gen.transaction.v1.OcpTransactionService import com.codeinc.opencode.gen.transaction.v1.OcpTransactionService.OpenAccountsMetadata.AccountSet import com.getcode.ed25519.Ed25519.KeyPair import com.getcode.opencode.internal.solana.model.SwapId +import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram import com.getcode.opencode.model.accounts.AccountType import com.getcode.opencode.model.core.ID import com.getcode.opencode.model.financial.LocalFiat @@ -372,6 +373,7 @@ internal fun StatefulSwapRequest.stablecoinParams(): OcpTransactionService.State .setFeeAmount(this@stablecoinParams.feeAmount?.underlyingTokenAmount?.quarks ?: 0) .setFundingSource(OcpTransactionService.FundingSource.FUNDING_SOURCE_SUBMIT_INTENT) .setFundingId(details.fundingSource.id.base58) + .setSwapVersion(CoinbaseStableSwapperProgram.SERVER_SWAP_VERSION) } } } 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 a17f9d2460..766c543ae1 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,13 +172,6 @@ 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/CoinbaseSwapAccounts.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseSwapAccounts.kt index 3c9fdeb3d9..9f6ddc6f43 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,7 +4,6 @@ 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( @@ -13,7 +12,6 @@ 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( @@ -29,7 +27,6 @@ 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, @@ -37,7 +34,6 @@ 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.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgram.kt index bff8dbaa8f..79c4f24dc2 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgram.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgram.kt @@ -5,5 +5,13 @@ import com.getcode.solana.keys.PublicKey internal class CoinbaseStableSwapperProgram { companion object { val address = PublicKey.fromBase58("pqgqKahpG1y2wsgxFhzaAnkV1cL9vk8MSg9qm4q646F") + + /** + * The `swap_version` sent on server-mediated stablecoin swap requests, and the instruction + * the client builds locally for the same transaction. Both must come from this one value: + * the server builds with the version we request, so a mismatch fails the swap. + * Exactly 2 selects SwapV2; anything else (including unset) is the legacy Swap. + */ + const val SERVER_SWAP_VERSION: Int = 2 } } 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_SwapV2.kt similarity index 86% rename from services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgram_Swap.kt rename to services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/programs/CoinbaseStableSwapperProgram_SwapV2.kt index c156d67ed9..0f0c706c79 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_SwapV2.kt @@ -5,7 +5,11 @@ import com.getcode.solana.keys.AccountMeta import com.getcode.solana.keys.PublicKey import com.getcode.utils.bytes -internal class CoinbaseStableSwapperProgram_Swap( +/** + * CoinbaseStableSwapper::SwapV2 (`swap_v2`), the only swap instruction the app builds. It takes + * 15 accounts; the retired v1 `swap` also took a whitelist account. + */ +internal class CoinbaseStableSwapperProgram_SwapV2( private val pool: PublicKey, private val inVault: PublicKey, private val outVault: PublicKey, @@ -18,7 +22,6 @@ 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 { @@ -38,7 +41,6 @@ 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), @@ -48,7 +50,6 @@ internal class CoinbaseStableSwapperProgram_Swap( } override fun encode(): List = buildList { - // 8-byte Anchor discriminator: [248, 198, 158, 145, 225, 117, 135, 200] addAll(DISCRIMINATOR.toList()) addAll(amountIn.bytes) addAll(minAmountOut.bytes) @@ -56,8 +57,7 @@ internal class CoinbaseStableSwapperProgram_Swap( companion object { private val DISCRIMINATOR = byteArrayOf( - 248.toByte(), 198.toByte(), 158.toByte(), 145.toByte(), - 225.toByte(), 117.toByte(), 135.toByte(), 200.toByte(), + 43, 4, 237.toByte(), 11, 26, 201.toByte(), 30, 98, ) } } 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 3d78a61738..61d16ffce0 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 @@ -3,7 +3,7 @@ package com.getcode.opencode.solana.swap import com.getcode.opencode.internal.solana.extensions.timelockSwapAccounts import com.getcode.opencode.internal.solana.model.CoinbaseSwapAccounts import com.getcode.opencode.internal.solana.programs.AssociatedTokenProgram_CreateIdempotent -import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram_Swap +import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram_SwapV2 import com.getcode.opencode.internal.solana.programs.ComputeBudgetProgram_SetComputeUnitLimit import com.getcode.opencode.internal.solana.programs.ComputeBudgetProgram_SetComputeUnitPrice import com.getcode.opencode.internal.solana.programs.MemoProgram_Memo @@ -115,9 +115,9 @@ internal fun buildStablecoinSwapperInstructions( ).instruction() ) - // 8. CoinbaseStableSwapper::Swap (from_mint swap authority ATA -> to_mint destination owner ATA) + // 8. CoinbaseStableSwapper::SwapV2 (from_mint swap authority ATA -> to_mint destination owner ATA) add( - CoinbaseStableSwapperProgram_Swap( + CoinbaseStableSwapperProgram_SwapV2( pool = swapAccounts.pool, inVault = swapAccounts.inVault, outVault = swapAccounts.outVault, @@ -130,7 +130,6 @@ 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 6dcff213eb..bca194f3b4 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 @@ -5,7 +5,7 @@ import com.getcode.opencode.internal.solana.extensions.deriveDepositAccount import com.getcode.opencode.internal.solana.extensions.deriveVirtualMachineAccount import com.getcode.opencode.internal.solana.model.CoinbaseSwapAccounts import com.getcode.opencode.internal.solana.programs.AssociatedTokenProgram_CreateIdempotent -import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram_Swap +import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram_SwapV2 import com.getcode.opencode.internal.solana.programs.ComputeBudgetProgram_SetComputeUnitLimit import com.getcode.opencode.internal.solana.programs.ComputeBudgetProgram_SetComputeUnitPrice import com.getcode.opencode.internal.solana.programs.MemoProgram_Memo @@ -23,7 +23,7 @@ import com.getcode.solana.keys.PublicKey * 2. [Optional] ComputeBudget::SetComputeUnitPrice * 3. [Optional] Memo::Memo * 4. AssociatedTokenAccount::CreateIdempotent (open owner's to_mint VM Deposit ATA) - * 5. CoinbaseStableSwapper::Swap (owner's from_mint ATA -> owner's to_mint VM Deposit ATA) + * 5. CoinbaseStableSwapper::SwapV2 (owner's from_mint ATA -> owner's to_mint VM Deposit ATA) * * Used by the on-app-open USDC -> USDF sweep: source is the owner's plain * USDC ATA, destination is the owner's USDF VM Deposit ATA (Geyser-monitored @@ -96,9 +96,9 @@ internal fun buildStatelessSwapInstructions( ).instruction() ) - // 5. CoinbaseStableSwapper::Swap (owner's from_mint ATA -> owner's to_mint VM Deposit ATA) + // 5. CoinbaseStableSwapper::SwapV2 (owner's from_mint ATA -> owner's to_mint VM Deposit ATA) add( - CoinbaseStableSwapperProgram_Swap( + CoinbaseStableSwapperProgram_SwapV2( pool = swapAccounts.pool, inVault = swapAccounts.inVault, outVault = swapAccounts.outVault, @@ -111,7 +111,6 @@ 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 2d4d2b27df..492214d17c 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 @@ -4,7 +4,7 @@ import com.getcode.opencode.internal.solana.extensions.timelockSwapAccounts import com.getcode.opencode.internal.solana.model.CoinbaseSwapAccounts import com.getcode.opencode.internal.solana.model.SwapId import com.getcode.opencode.internal.solana.programs.AssociatedTokenProgram_CreateIdempotent -import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram_Swap +import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram_SwapV2 import com.getcode.opencode.internal.solana.programs.ComputeBudgetProgram_SetComputeUnitLimit import com.getcode.opencode.internal.solana.programs.ComputeBudgetProgram_SetComputeUnitPrice import com.getcode.opencode.internal.solana.programs.MemoProgram_Memo @@ -68,7 +68,7 @@ internal fun buildUsdcToUsdfSwapInstructions( ).instruction() ) - // 7. Swap (USDC ATA -> USDF ATA) + // 7. Swap (USDC ATA -> USDF ATA); the Coinbase pool uses CoinbaseStableSwapper::SwapV2 when (pool) { is FundSwapPool.Usdf -> { add( @@ -87,7 +87,7 @@ internal fun buildUsdcToUsdfSwapInstructions( is FundSwapPool.CoinbaseStableSwapper -> { val swapAccounts = CoinbaseSwapAccounts.derive(Mint.usdc, Mint.usdf) add( - CoinbaseStableSwapperProgram_Swap( + CoinbaseStableSwapperProgram_SwapV2( pool = swapAccounts.pool, inVault = swapAccounts.inVault, outVault = swapAccounts.outVault, @@ -103,7 +103,6 @@ internal fun buildUsdcToUsdfSwapInstructions( fromMint = Mint.usdc, toMint = Mint.usdf, user = sender, - whitelist = swapAccounts.whitelist, amountIn = amount, minAmountOut = amount, // 1:1 stable pair; the program rejects 0 with InvalidAmount ).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 0fd537cd85..f423ca5962 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 @@ -5,7 +5,7 @@ import com.getcode.opencode.internal.solana.extensions.deriveDepositAccount import com.getcode.opencode.internal.solana.extensions.deriveVirtualMachineAccount import com.getcode.opencode.internal.solana.model.CoinbaseSwapAccounts import com.getcode.opencode.internal.solana.programs.AssociatedTokenProgram_CreateIdempotent -import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram_Swap +import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram_SwapV2 import com.getcode.opencode.internal.solana.programs.ComputeBudgetProgram_SetComputeUnitLimit import com.getcode.opencode.internal.solana.programs.ComputeBudgetProgram_SetComputeUnitPrice import com.getcode.opencode.internal.solana.vmAuthority @@ -24,7 +24,7 @@ import com.getcode.solana.keys.PublicKey * 1. ComputeBudget::SetComputeUnitLimit(200_000) * 2. ComputeBudget::SetComputeUnitPrice(1_000) * 3. AssociatedTokenAccount::CreateIdempotent (owner's USDF VM deposit PDA ATA — Phantom pays rent) - * 4. CoinbaseStableSwapper::Swap (sender's USDC ATA → owner's USDF VM deposit PDA ATA) + * 4. CoinbaseStableSwapper::SwapV2 (sender's USDC ATA → owner's USDF VM deposit PDA ATA) * * The Geyser watcher monitors the deposit PDA ATA and transfers USDF * into the VM once funds land — same path as the stateless sweep. @@ -82,9 +82,9 @@ internal fun buildUsdfDepositInstructions( mint = Mint.usdf, ).instruction() ) - // 4. CoinbaseStableSwapper::Swap (sender's USDC ATA → owner's USDF VM deposit PDA ATA) + // 4. CoinbaseStableSwapper::SwapV2 (sender's USDC ATA → owner's USDF VM deposit PDA ATA) add( - CoinbaseStableSwapperProgram_Swap( + CoinbaseStableSwapperProgram_SwapV2( pool = swapAccounts.pool, inVault = swapAccounts.inVault, outVault = swapAccounts.outVault, @@ -97,7 +97,6 @@ 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 02b327010f..c199c269c9 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,22 +247,6 @@ 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 @@ -271,11 +255,10 @@ 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 four should be distinct - val all = setOf(pool, vault, vaultTA, whitelist) - assertEquals(4, all.size, "pool, vault, vaultTA, and whitelist should all be distinct") + // All three should be distinct + val all = setOf(pool, vault, vaultTA) + assertEquals(3, all.size, "pool, vault, and vaultTA should all be distinct") } // --- Known value: well-known associated token address --- 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 92bbfff95f..929a3318f9 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 @@ -22,184 +22,47 @@ class CoinbaseStableSwapperProgramTest { ) } - // --- Swap instruction discriminator --- + // --- SwapV2 --- - @Test - fun swapDiscriminatorBytes() { - 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), whitelist = testKey(13), - amountIn = 0L, minAmountOut = 0L, - ) - val encoded = ix.encode() - // Anchor discriminator: [248, 198, 158, 145, 225, 117, 135, 200] - assertEquals(248.toByte(), encoded[0]) - assertEquals(198.toByte(), encoded[1]) - assertEquals(158.toByte(), encoded[2]) - assertEquals(145.toByte(), encoded[3]) - assertEquals(225.toByte(), encoded[4]) - assertEquals(117.toByte(), encoded[5]) - assertEquals(135.toByte(), encoded[6]) - assertEquals(200.toByte(), encoded[7]) + private val goldenKeys = (0..11).map { i -> + PublicKey(ByteArray(32) { if (it == 0) (i + 1).toByte() else 0 }.toList()) } - // --- Swap instruction encoding --- + private fun goldenV2() = CoinbaseStableSwapperProgram_SwapV2( + pool = goldenKeys[0], inVault = goldenKeys[1], outVault = goldenKeys[2], + inVaultTokenAccount = goldenKeys[3], outVaultTokenAccount = goldenKeys[4], + userFromTokenAccount = goldenKeys[5], toTokenAccount = goldenKeys[6], + feeRecipientTokenAccount = goldenKeys[7], feeRecipient = goldenKeys[8], + fromMint = goldenKeys[9], toMint = goldenKeys[10], + user = goldenKeys[11], + amountIn = 1_500_000L, minAmountOut = 1_490_000L, + ).instruction() @Test - fun swapEncodeLength() { - 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), whitelist = testKey(13), - amountIn = 1000L, minAmountOut = 900L, - ) - // 8 bytes discriminator + 8 bytes amountIn + 8 bytes minAmountOut = 24 - assertEquals(24, ix.encode().size) - } - - @Test - fun swapEncodesAmountsAsLittleEndian() { - 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), whitelist = testKey(13), - amountIn = 1_000_000L, minAmountOut = 999_000L, - ) - val encoded = ix.encode() - - // amountIn = 1_000_000 = 0x000F4240 LE → [0x40, 0x42, 0x0F, 0x00, ...] - assertEquals(0x40.toByte(), encoded[8]) - assertEquals(0x42.toByte(), encoded[9]) - assertEquals(0x0F.toByte(), encoded[10]) - for (i in 11..15) assertEquals(0x00.toByte(), encoded[i]) - - // minAmountOut = 999_000 = 0x000F3E58 LE → [0x58, 0x3E, 0x0F, 0x00, ...] - assertEquals(0x58.toByte(), encoded[16]) - assertEquals(0x3E.toByte(), encoded[17]) - assertEquals(0x0F.toByte(), encoded[18]) - for (i in 19..23) assertEquals(0x00.toByte(), encoded[i]) + fun swapV2GoldenData() { + val expected = listOf( + 43, 4, 237, 11, 26, 201, 30, 98, + 0x60, 0xe3, 0x16, 0, 0, 0, 0, 0, + 0x50, 0xbc, 0x16, 0, 0, 0, 0, 0, + ).map { it.toByte() } + assertEquals(expected, goldenV2().data) } - // --- Swap instruction accounts --- - @Test - 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), whitelist = testKey(13), - amountIn = 1000L, minAmountOut = 900L, - ) - assertEquals(16, ix.instruction().accounts.size) + fun swapV2AccountsHaveNoWhitelist() { + val v2 = goldenV2() + assertEquals(15, v2.accounts.size) + assertEquals(goldenKeys.take(12), v2.accounts.take(12).map { it.publicKey }) + assertEquals(CoinbaseStableSwapperProgram.address, v2.program) } @Test - fun swapInstructionProgram() { - 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), whitelist = testKey(13), - amountIn = 1000L, minAmountOut = 900L, - ) - assertEquals(CoinbaseStableSwapperProgram.address, ix.instruction().program) - } - - @Test - fun swapAccountOrder() { - 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], 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, whitelist, - // tokenProgram, associatedTokenProgram, systemProgram - assertEquals(keys[0], accounts[0].publicKey) // pool - assertEquals(keys[1], accounts[1].publicKey) // inVault - assertEquals(keys[2], accounts[2].publicKey) // outVault - assertEquals(keys[3], accounts[3].publicKey) // inVaultTokenAccount - assertEquals(keys[4], accounts[4].publicKey) // outVaultTokenAccount - assertEquals(keys[5], accounts[5].publicKey) // userFromTokenAccount - assertEquals(keys[6], accounts[6].publicKey) // toTokenAccount - assertEquals(keys[7], accounts[7].publicKey) // feeRecipientTokenAccount - assertEquals(keys[8], accounts[8].publicKey) // feeRecipient - assertEquals(keys[9], accounts[9].publicKey) // fromMint - assertEquals(keys[10], accounts[10].publicKey) // toMint - assertEquals(keys[11], accounts[11].publicKey) // user - 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 - fun swapAccountSignerFlags() { - 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), whitelist = testKey(13), - amountIn = 1000L, minAmountOut = 900L, - ) - val accounts = ix.instruction().accounts - - // Only user (index 11) is a signer - for (i in accounts.indices) { - if (i == 11) { - assertTrue(accounts[i].isSigner, "user (index $i) should be signer") - } else { - assertFalse(accounts[i].isSigner, "account at index $i should not be signer") - } - } - } - - @Test - fun swapAccountWritableFlags() { - 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), whitelist = testKey(13), - amountIn = 1000L, minAmountOut = 900L, - ) - val accounts = ix.instruction().accounts - - // Writable: inVaultTokenAccount(3), outVaultTokenAccount(4), userFromTokenAccount(5), - // toTokenAccount(6), feeRecipientTokenAccount(7), user(11) - val writableIndices = setOf(3, 4, 5, 6, 7, 11) - for (i in accounts.indices) { - if (i in writableIndices) { - assertTrue(accounts[i].isWritable, "account at index $i should be writable") - } else { - assertFalse(accounts[i].isWritable, "account at index $i should not be writable") - } - } + fun swapV2TailAccountsAndFlags() { + val a = goldenV2().accounts + assertEquals(TokenProgram.address, a[12].publicKey) + assertEquals(AssociatedTokenProgram.address, a[13].publicKey) + assertEquals(SystemProgram.address, a[14].publicKey) + assertEquals(setOf(11), a.indices.filter { a[it].isSigner }.toSet()) + assertEquals(setOf(3, 4, 5, 6, 7, 11), a.indices.filter { a[it].isWritable }.toSet()) } } diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/ClientBuiltDepositSwapV2Test.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/ClientBuiltDepositSwapV2Test.kt new file mode 100644 index 0000000000..57f3114507 --- /dev/null +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/ClientBuiltDepositSwapV2Test.kt @@ -0,0 +1,44 @@ +package com.getcode.opencode.solana.swap + +import com.getcode.opencode.internal.solana.model.SwapId +import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram +import com.getcode.opencode.model.transactions.FundSwapPool +import com.getcode.solana.keys.PublicKey +import kotlin.test.Test +import kotlin.test.assertEquals + +/** The client-built deposit swaps must use CoinbaseStableSwapper SwapV2: 15 accounts, v2 discriminator. */ +class ClientBuiltDepositSwapV2Test { + + private val sender = PublicKey(ByteArray(32) { 3 }.toList()) + private val owner = PublicKey(ByteArray(32) { 7 }.toList()) + private val feeRecipient = PublicKey(ByteArray(32) { 5 }.toList()) + private val v2Discriminator = listOf(43, 4, -19, 11, 26, -55, 30, 98) + + @Test + fun `usdf deposit swap instruction is SwapV2`() { + val ix = buildUsdfDepositInstructions( + sender = sender, + owner = owner, + amount = 1_000_000L, + feeRecipient = feeRecipient, + ).single { it.program == CoinbaseStableSwapperProgram.address } + + assertEquals(15, ix.accounts.size) + assertEquals(v2Discriminator, ix.data.take(8)) + } + + @Test + fun `usdc to usdf coinbase swap instruction is SwapV2`() { + val ix = buildUsdcToUsdfSwapInstructions( + sender = sender, + owner = owner, + amount = 1_000_000L, + pool = FundSwapPool.CoinbaseStableSwapper(feeRecipient = feeRecipient), + swapId = SwapId(PublicKey(ByteArray(32) { 9 }.toList())), + ).single { it.program == CoinbaseStableSwapperProgram.address } + + assertEquals(15, ix.accounts.size) + assertEquals(v2Discriminator, ix.data.take(8)) + } +} diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinDepositTransactionTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinDepositTransactionTest.kt index 6248ab8ef6..1098edf749 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinDepositTransactionTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/CoinbaseStablecoinDepositTransactionTest.kt @@ -20,10 +20,11 @@ import kotlin.test.assertTrue * valid-looking [PublicKey] and a transaction that encodes fine, so only the end-to-end bytes * catch it. * - * The fixture below was accepted by `simulateTransaction` on mainnet (no error, 64422 compute - * units, `Swapped 5000000 tokens`). Two earlier shapes of this same transaction were rejected - * there: `fee_recipient` read at the offset the published IDL implies fails with - * `ConstraintAddress` (2012), and `minAmountOut = 0` fails with `InvalidAmount` (6002). + * The deposit swap is `swap_v2` (15 accounts). These bytes were accepted by + * `simulateTransaction` on mainnet (no error, 59730 compute units, `Swapped 5000000 tokens`). + * Two earlier shapes of this transaction were rejected there: `fee_recipient` read at the offset + * the published IDL implies fails with `ConstraintAddress` (2012), and `minAmountOut = 0` fails + * with `InvalidAmount` (6002). */ class CoinbaseStablecoinDepositTransactionTest { @@ -46,21 +47,24 @@ class CoinbaseStablecoinDepositTransactionTest { private val blockhash = Hash(ByteArray(32) { 99 }.toList()) private val amount = 5_000_000L - /** Any change here is a wire-format change on the deposit path. Re-simulate before updating. */ + /** + * Mainnet-simulated `swap_v2` bytes; see the class comment. Any change here is a wire-format + * change on the deposit path. Re-simulate before updating. + */ private val expectedTransactionBase64 = - "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" private fun pool(): FundSwapPool.CoinbaseStableSwapper = FundSwapPool.CoinbaseStableSwapper.fromAccountData( 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 00896e1d95..adece301b2 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,7 +3,6 @@ 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 @@ -255,10 +254,11 @@ class CoinbaseStablecoinSwapperInstructionsTest { } @Test - fun `swap instruction has correct whitelist PDA`() { + fun `swap instruction is SwapV2 with 15 accounts`() { val ix = buildInstructions()[7] - val expectedWhitelist = PublicKey.deriveCoinbaseWhitelistAddress().publicKey - assertEquals(expectedWhitelist, ix.accounts[12].publicKey) + assertEquals(15, ix.accounts.size) + assertEquals(CoinbaseStableSwapperProgram.SERVER_SWAP_VERSION, 2) + assertEquals(listOf(43, 4, -19, 11, 26, -55, 30, 98), ix.data.take(8)) } // --- CloseAccount verification --- diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/StablecoinSwapVersionRequestTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/StablecoinSwapVersionRequestTest.kt new file mode 100644 index 0000000000..ad8168edc2 --- /dev/null +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/solana/swap/StablecoinSwapVersionRequestTest.kt @@ -0,0 +1,79 @@ +package com.getcode.opencode.solana.swap + +import com.getcode.ed25519.Ed25519.KeyPair +import com.getcode.opencode.internal.network.api.intents.IntentStatelessSwap +import com.getcode.opencode.internal.network.extensions.stablecoinParams +import com.getcode.opencode.internal.solana.model.SwapId +import com.getcode.opencode.internal.manager.VerifiedState +import com.getcode.opencode.internal.solana.programs.CoinbaseStableSwapperProgram +import com.getcode.opencode.model.accounts.AccountCluster +import com.getcode.opencode.model.financial.Fiat +import com.getcode.opencode.model.financial.LocalFiat +import com.getcode.opencode.model.financial.Rate +import com.getcode.opencode.model.transactions.StatefulSwapRequest +import com.getcode.opencode.model.transactions.StatelessSwapRequest +import com.getcode.opencode.model.transactions.SwapFundingSource +import com.getcode.opencode.model.transactions.SwapProgram +import com.getcode.opencode.model.transactions.SwapRoute +import com.getcode.solana.keys.Mint +import com.getcode.solana.keys.PublicKey +import io.mockk.every +import io.mockk.mockk +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * The server builds the stablecoin swap with the version we request and we rebuild it locally, so + * the request's swap_version and the locally built instruction must come from one value. + */ +class StablecoinSwapVersionRequestTest { + + private fun key(seed: Int) = PublicKey(ByteArray(32) { seed.toByte() }.toList()) + + @Test + fun `swap version constant is 2`() { + assertEquals(2, CoinbaseStableSwapperProgram.SERVER_SWAP_VERSION) + } + + @Test + fun `stateful stablecoin initiate params carry swap_version 2`() { + val request = StatefulSwapRequest( + owner = mockk(relaxed = true), + swapAuthority = mockk(relaxed = true) { + every { publicKeyBytes } returns ByteArray(32) { 21 } + }, + program = SwapProgram.Stablecoin( + fromMint = key(1), + toMint = key(2), + destinationOwner = key(3), + fundingSource = SwapFundingSource.SubmitIntent(key(30).bytes), + ), + route = mockk(relaxed = true), + swapAmount = LocalFiat( + underlyingTokenAmount = Fiat(quarks = 1_000_000), + nativeAmount = Fiat(quarks = 1_000_000), + rate = Rate.oneToOne, + mint = Mint.usdf, + ), + feeAmount = null, + swapId = SwapId(key(31).bytes), + verifiedState = mockk(relaxed = true), + ) + + assertEquals(2, request.stablecoinParams().build().swapVersion) + } + + @Test + fun `stateless stablecoin initiate params carry swap_version 2`() { + val intent = IntentStatelessSwap( + StatelessSwapRequest( + owner = mockk(relaxed = true), + fromMint = Mint(key(1).bytes), + toMint = Mint(key(2).bytes), + amount = 1_000_000, + ) + ) + + assertEquals(2, intent.stablecoinParams().swapVersion) + } +} 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 37cb7acfee..eaa471c7ac 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,7 +4,6 @@ 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 @@ -297,10 +296,10 @@ class StatelessSwapInstructionsTest { } @Test - fun `swap instruction has correct whitelist PDA`() { + fun `swap instruction is SwapV2 with 15 accounts`() { val ix = buildInstructions()[4] - val expectedWhitelist = PublicKey.deriveCoinbaseWhitelistAddress().publicKey - assertEquals(expectedWhitelist, ix.accounts[12].publicKey) + assertEquals(15, ix.accounts.size) + assertEquals(listOf(43, 4, -19, 11, 26, -55, 30, 98), ix.data.take(8)) } // --- CreateIdempotent and Swap destination linkage ---