diff --git a/apps/flipcash/shared/onramp/deeplinks/build.gradle.kts b/apps/flipcash/shared/onramp/deeplinks/build.gradle.kts index d17887924..f14806af3 100644 --- a/apps/flipcash/shared/onramp/deeplinks/build.gradle.kts +++ b/apps/flipcash/shared/onramp/deeplinks/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(libs.bundles.kotlinx.serialization) implementation(project(":apps:flipcash:shared:analytics")) + implementation(project(":apps:flipcash:shared:userflags")) implementation(project(":libs:crypto:solana")) implementation(project(":libs:messaging")) } diff --git a/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/ExternalWalletOnRampController.kt b/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/ExternalWalletOnRampController.kt index 4ff2dd437..443685b16 100644 --- a/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/ExternalWalletOnRampController.kt +++ b/apps/flipcash/shared/onramp/deeplinks/src/main/kotlin/com/flipcash/app/onramp/ExternalWalletOnRampController.kt @@ -5,15 +5,18 @@ import com.flipcash.app.core.encryption.boxOpen import com.flipcash.app.core.navigation.DeeplinkType import com.flipcash.app.core.onramp.deeplinks.ExternalWalletConnection import com.flipcash.app.core.onramp.deeplinks.ExternallySignedTransaction +import com.flipcash.app.userflags.UserFlagsCoordinator import com.flipcash.services.internal.model.thirdparty.OnRampProvider +import com.flipcash.services.internal.model.thirdparty.UsdcLiquidtyPool import com.flipcash.services.user.UserManager import com.getcode.opencode.controllers.TransactionOperations import com.getcode.opencode.exchange.VerifiedFiat import com.getcode.opencode.internal.solana.extensions.deriveAssociatedAccount -import com.getcode.opencode.internal.solana.model.LiquidityPool import com.getcode.opencode.internal.solana.model.SwapId import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.Token +import com.getcode.opencode.model.transactions.FundSwapPool +import com.getcode.opencode.model.transactions.LiquidityPool import com.getcode.opencode.model.transactions.SwapFundingSource import com.getcode.opencode.solana.SolanaTransaction import com.getcode.opencode.solana.TransactionBuilder @@ -29,6 +32,7 @@ import com.getcode.solana.rpc.RpcException import com.getcode.solana.rpc.SolanaConnection import com.getcode.solana.rpc.doesAccountExist import com.getcode.solana.rpc.getBalance +import com.getcode.solana.rpc.getAccountData import com.getcode.solana.rpc.getTokenAccountBalance import com.getcode.solana.rpc.sendTransaction import com.getcode.solana.rpc.simulateTransaction @@ -52,6 +56,8 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import javax.inject.Inject @@ -59,6 +65,7 @@ import javax.inject.Inject @ActivityRetainedScoped class ExternalWalletOnRampController @Inject constructor( private val userManager: UserManager, + private val userFlags: UserFlagsCoordinator, private val transactionController: TransactionOperations, private val rpcConfig: RpcConfig, ) { @@ -445,11 +452,25 @@ class ExternalWalletOnRampController @Inject constructor( val swapId = SwapId.generate() val recentBlockhash = connection.getLatestBlockhash() + + val liquidityPool = userFlags.resolvedFlags.value.usdcOnRampLiquidityPool.effectiveValue + val swapPool = when (liquidityPool) { + UsdcLiquidtyPool.CoinbaseStableSwapper -> { + val poolAddress = FundSwapPool.CoinbaseStableSwapper.poolAddress + val poolData = driver.getAccountData(poolAddress).getOrThrow() + FundSwapPool.CoinbaseStableSwapper.fromAccountData(poolData) + } + UsdcLiquidtyPool.Unknown, + UsdcLiquidtyPool.Flipcash -> FundSwapPool.Usdf(LiquidityPool.usdf) + } + + trace("Building USDC -> USDF fund swap using $liquidityPool LP") + val transaction = TransactionBuilder.usdcFundSwap( owner = owner.authorityPublicKey, sender = sender, amount = amount.underlyingTokenAmount.quarks, - pool = LiquidityPool.usdf, + pool = swapPool, blockhash = Hash(recentBlockhash), swapId = swapId, ) diff --git a/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/internal/ExternalWalletBalanceCheckTest.kt b/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/internal/ExternalWalletBalanceCheckTest.kt index f1f09c5bd..7f1a0c3f2 100644 --- a/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/internal/ExternalWalletBalanceCheckTest.kt +++ b/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/internal/ExternalWalletBalanceCheckTest.kt @@ -5,6 +5,7 @@ import com.flipcash.app.core.onramp.deeplinks.ExternalWalletConnection import com.flipcash.app.onramp.DeeplinkOnRampError import com.flipcash.app.onramp.ExternalWalletOnRampController import com.flipcash.app.onramp.ExternalWalletOnRampState +import com.flipcash.app.userflags.UserFlagsCoordinator import com.flipcash.services.internal.model.thirdparty.OnRampProvider import com.flipcash.services.user.UserManager import com.getcode.opencode.controllers.TransactionOperations @@ -31,6 +32,7 @@ import kotlin.test.assertTrue class ExternalWalletBalanceCheckTest { private val userManager = mockk(relaxed = true) + private val userFlags = mockk(relaxed = true) private val transactionController = mockk(relaxed = true) private val networkDriver = mockk() private val rpcConfig = RpcConfig(networkDriver = networkDriver, rpcUrl = "https://localhost") @@ -45,6 +47,7 @@ class ExternalWalletBalanceCheckTest { every { Box.keypair() } returns mockk(relaxed = true) controller = ExternalWalletOnRampController( userManager = userManager, + userFlags = userFlags, transactionController = transactionController, rpcConfig = rpcConfig, ) diff --git a/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/internal/ExternalWalletDeeplinkStateErrorTest.kt b/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/internal/ExternalWalletDeeplinkStateErrorTest.kt index 6007fa024..71d97d2d2 100644 --- a/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/internal/ExternalWalletDeeplinkStateErrorTest.kt +++ b/apps/flipcash/shared/onramp/deeplinks/src/test/kotlin/com/flipcash/app/onramp/internal/ExternalWalletDeeplinkStateErrorTest.kt @@ -7,6 +7,7 @@ import com.flipcash.app.onramp.DeeplinkError import com.flipcash.app.onramp.DeeplinkOnRampError import com.flipcash.app.onramp.ExternalWalletOnRampController import com.flipcash.app.onramp.ExternalWalletOnRampState +import com.flipcash.app.userflags.UserFlagsCoordinator import com.flipcash.services.user.UserManager import com.getcode.opencode.controllers.TransactionOperations import com.getcode.solana.rpc.RpcConfig @@ -30,6 +31,8 @@ class ExternalWalletDeeplinkStateErrorTest { private val networkDriver = mockk(relaxed = true) private val rpcConfig = RpcConfig(networkDriver = networkDriver, rpcUrl = "https://localhost") + private val userFlags = mockk(relaxed = true) + private lateinit var controller: ExternalWalletOnRampController @Before @@ -38,6 +41,7 @@ class ExternalWalletDeeplinkStateErrorTest { every { Box.keypair() } returns mockk(relaxed = true) controller = ExternalWalletOnRampController( userManager = userManager, + userFlags = userFlags, transactionController = transactionController, rpcConfig = rpcConfig, ) diff --git a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt index 36a50146c..fea601a3f 100644 --- a/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt +++ b/libs/crypto/solana/src/main/kotlin/com/getcode/solana/rpc/Calls.kt @@ -4,10 +4,12 @@ import com.getcode.solana.keys.PublicKey import com.solana.networking.Rpc20Driver import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.long import org.sol4k.Connection +import android.util.Base64 class SolanaConnection(rpcUrl: String,) { private val connection = Connection(rpcUrl) @@ -92,6 +94,30 @@ suspend fun Rpc20Driver.doesAccountExist(publicKey: PublicKey): Result { return Result.success(Unit) } +/** + * Returns the raw account data for the given public key, base64-decoded. + */ +suspend fun Rpc20Driver.getAccountData(publicKey: PublicKey): Result { + val response = makeRequest( + request = GetAccountInfo(publicKey), + resultSerializer = JsonElement.serializer() + ) + val error = response.error + if (error != null) { + return Result.failure(RpcException(error.code, error.message)) + } + + val value = response.result?.jsonObject?.get("value")?.takeIf { it !is JsonNull } + ?: return Result.failure(Throwable("Account not found")) + + val dataArray = value.jsonObject["data"]?.jsonArray + ?: return Result.failure(Throwable("Missing account data")) + + val base64String = dataArray[0].jsonPrimitive.content + val decoded = Base64.decode(base64String, Base64.NO_WRAP) + return Result.success(decoded) +} + /** * Sends a transaction to the Solana blockchain. * 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 new file mode 100644 index 000000000..a23a5e114 --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseStablecoinPoolAccount.kt @@ -0,0 +1,27 @@ +package com.getcode.opencode.internal.solana.model + +import com.getcode.solana.keys.PublicKey + +/** + * Represents the on-chain CoinbaseStableSwapper liquidity pool account. + * + * Layout: + * [8 discriminator][32 operations_authority][32 pause_authority][32 fee_recipient]... + */ +internal data class CoinbaseStablecoinPoolAccount( + val feeRecipient: PublicKey, +) { + companion object { + private const val FEE_RECIPIENT_OFFSET = 8 + 32 + 32 // discriminator + ops_authority + pause_authority + + fun fromAccountData(data: ByteArray): CoinbaseStablecoinPoolAccount { + 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( + feeRecipient = PublicKey(feeRecipientBytes.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 new file mode 100644 index 000000000..3c9fdeb3d --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/CoinbaseSwapAccounts.kt @@ -0,0 +1,44 @@ +package com.getcode.opencode.internal.solana.model + +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( + val pool: PublicKey, + val inVault: PublicKey, + val outVault: PublicKey, + val inVaultTokenAccount: PublicKey, + val outVaultTokenAccount: PublicKey, + val whitelist: PublicKey, +) { + fun feeRecipientTokenAccount(feeRecipient: PublicKey, fromMint: PublicKey): PublicKey { + return PublicKey.deriveAssociatedAccount( + owner = feeRecipient, + mint = fromMint, + ).publicKey + } + + companion object { + fun derive(fromMint: PublicKey, toMint: PublicKey): CoinbaseSwapAccounts { + val pool = PublicKey.deriveCoinbasePoolAddress().publicKey + val inVault = PublicKey.deriveCoinbaseTokenVaultAddress(pool, fromMint).publicKey + 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, + inVault = inVault, + outVault = outVault, + inVaultTokenAccount = inVaultTokenAccount, + outVaultTokenAccount = outVaultTokenAccount, + whitelist = whitelist, + ) + } + } +} diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/FundSwapPool.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/FundSwapPool.kt new file mode 100644 index 000000000..2fb5e8b70 --- /dev/null +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/FundSwapPool.kt @@ -0,0 +1,25 @@ +package com.getcode.opencode.model.transactions + +import com.getcode.opencode.internal.solana.extensions.deriveCoinbasePoolAddress +import com.getcode.opencode.internal.solana.model.CoinbaseStablecoinPoolAccount +import com.getcode.solana.keys.PublicKey + +sealed interface FundSwapPool { + data class Usdf(val pool: LiquidityPool) : FundSwapPool + data class CoinbaseStableSwapper(val feeRecipient: PublicKey) : FundSwapPool { + companion object { + /** The on-chain address of the Coinbase liquidity pool account. */ + val poolAddress: PublicKey + get() = PublicKey.deriveCoinbasePoolAddress().publicKey + + /** + * Parses the on-chain pool account data and returns a [CoinbaseStableSwapper] + * with the resolved fee recipient. + */ + fun fromAccountData(data: ByteArray): CoinbaseStableSwapper { + val poolAccount = CoinbaseStablecoinPoolAccount.fromAccountData(data) + return CoinbaseStableSwapper(feeRecipient = poolAccount.feeRecipient) + } + } + } +} \ No newline at end of file diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/LiquidityPool.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/LiquidityPool.kt similarity index 91% rename from services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/LiquidityPool.kt rename to services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/LiquidityPool.kt index d45d6d089..a5b393792 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/solana/model/LiquidityPool.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/model/transactions/LiquidityPool.kt @@ -1,13 +1,12 @@ -package com.getcode.opencode.internal.solana.model +package com.getcode.opencode.model.transactions +import com.getcode.opencode.internal.solana.model.Vault import com.getcode.opencode.internal.solana.vmAuthority import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey import com.getcode.utils.serializer.PublicKeyAsStringSerializer -import com.getcode.vendor.Base58 import kotlinx.serialization.Serializable - @Serializable(with = PublicKeyAsStringSerializer::class) class LiquidityPool( val address: PublicKey, diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/TransactionBuilder.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/TransactionBuilder.kt index d637e2508..93a03328c 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/solana/TransactionBuilder.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/solana/TransactionBuilder.kt @@ -1,12 +1,12 @@ package com.getcode.opencode.solana -import com.getcode.opencode.internal.solana.model.LiquidityPool import com.getcode.opencode.internal.solana.model.SwapId import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.financial.usdf import com.getcode.opencode.model.transactions.SwapDirection import com.getcode.opencode.model.transactions.SwapResponseServerParameters import com.getcode.opencode.model.financial.MintMetadata +import com.getcode.opencode.model.transactions.FundSwapPool import com.getcode.opencode.solana.swap.buildExistingCurrencyBuyInstructions import com.getcode.opencode.solana.swap.buildNewCurrencyBuyInstructions import com.getcode.opencode.solana.swap.buildSellInstructions @@ -179,7 +179,7 @@ object TransactionBuilder { * @param owner The public key of the wallet owner whose USDF swap PDA will receive the funds. * @param sender The public key of the account paying for and signing the transaction. * @param amount The amount of USDC to swap into USDF (in quarks). - * @param pool The USDF liquidity pool containing vault addresses for the swap. + * @param pool The pool to use for the swap — either USDF liquidity pool or CoinbaseStableSwapper. * @param swapId A unique identifier for this swap, included as a memo in the transaction. * @param blockhash A recent blockhash for the transaction, or null. * @return A constructed [SolanaTransaction] (V0) ready to be signed and submitted to the network. @@ -188,7 +188,7 @@ object TransactionBuilder { owner: PublicKey, sender: PublicKey, amount: Long, - pool: LiquidityPool, + pool: FundSwapPool, swapId: SwapId, blockhash: Hash?, ): SolanaTransaction { 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 2487e9143..b9d8d9d23 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 @@ -1,11 +1,7 @@ package com.getcode.opencode.solana.swap -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.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.ComputeBudgetProgram_SetComputeUnitLimit @@ -63,17 +59,12 @@ internal fun buildStablecoinSwapperInstructions( val fromTimelockAccounts = fromMintMetadata.timelockSwapAccounts(authority) // Derive CoinbaseStableSwapper PDAs - val pool = PublicKey.deriveCoinbasePoolAddress().publicKey - val inVault = PublicKey.deriveCoinbaseTokenVaultAddress(pool, fromMintMetadata.address).publicKey - val outVault = PublicKey.deriveCoinbaseTokenVaultAddress(pool, toMintMetadata.address).publicKey - val inVaultTokenAccount = PublicKey.deriveCoinbaseVaultTokenAccountAddress(inVault).publicKey - val outVaultTokenAccount = PublicKey.deriveCoinbaseVaultTokenAccountAddress(outVault).publicKey - val whitelist = PublicKey.deriveCoinbaseWhitelistAddress().publicKey + val swapAccounts = CoinbaseSwapAccounts.derive(fromMintMetadata.address, toMintMetadata.address) - val feeRecipientFromMintAta = PublicKey.deriveAssociatedAccount( - owner = serverParameters.poolFeeRecipient, - mint = fromMintMetadata.address, - ).publicKey + val feeRecipientFromMintAta = swapAccounts.feeRecipientTokenAccount( + feeRecipient = serverParameters.poolFeeRecipient, + fromMint = fromMintMetadata.address, + ) // 5. AssociatedTokenAccount::CreateIdempotent (open swap authority's from_mint ATA) val createSwapAuthorityFromMintAta = AssociatedTokenProgram_CreateIdempotent( @@ -127,11 +118,11 @@ internal fun buildStablecoinSwapperInstructions( // 8. CoinbaseStableSwapper::Swap (from_mint swap authority ATA -> to_mint destination owner ATA) add( CoinbaseStableSwapperProgram_Swap( - pool = pool, - inVault = inVault, - outVault = outVault, - inVaultTokenAccount = inVaultTokenAccount, - outVaultTokenAccount = outVaultTokenAccount, + pool = swapAccounts.pool, + inVault = swapAccounts.inVault, + outVault = swapAccounts.outVault, + inVaultTokenAccount = swapAccounts.inVaultTokenAccount, + outVaultTokenAccount = swapAccounts.outVaultTokenAccount, userFromTokenAccount = createSwapAuthorityFromMintAta.address, toTokenAccount = createDestinationOwnerToMintAta.address, feeRecipientTokenAccount = feeRecipientFromMintAta, @@ -139,7 +130,7 @@ internal fun buildStablecoinSwapperInstructions( fromMint = fromMintMetadata.address, toMint = toMintMetadata.address, user = swapAuthority, - whitelist = whitelist, + whitelist = swapAccounts.whitelist, amountIn = amount, minAmountOut = minOutput, ).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 85fb6dff5..62243ecd1 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 @@ -1,9 +1,10 @@ package com.getcode.opencode.solana.swap import com.getcode.opencode.internal.solana.extensions.timelockSwapAccounts -import com.getcode.opencode.internal.solana.model.LiquidityPool +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.ComputeBudgetProgram_SetComputeUnitLimit import com.getcode.opencode.internal.solana.programs.ComputeBudgetProgram_SetComputeUnitPrice import com.getcode.opencode.internal.solana.programs.MemoProgram_Memo @@ -11,6 +12,8 @@ import com.getcode.opencode.internal.solana.programs.TokenProgram_Transfer import com.getcode.opencode.internal.solana.programs.UsdfProgram_Swap import com.getcode.opencode.model.financial.Token import com.getcode.opencode.model.financial.usdf +import com.getcode.opencode.model.transactions.FundSwapPool +import com.getcode.opencode.model.transactions.LiquidityPool import com.getcode.opencode.solana.Instruction import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey @@ -20,7 +23,7 @@ internal fun buildUsdcToUsdfSwapInstructions( sender: PublicKey, owner: PublicKey, amount: Long, - pool: LiquidityPool, + pool: FundSwapPool, swapId: SwapId, ): List { return buildList { @@ -65,19 +68,48 @@ internal fun buildUsdcToUsdfSwapInstructions( ).instruction() ) - // 7. Usdf::Swap (USDC ATA -> USDF ATA) - add( - UsdfProgram_Swap( - amount = amount, - usdfToOther = false, - user = sender, - pool = pool.address, - usdfVault = pool.usdfVault, - otherVault = pool.otherVault, - userUsdfToken = createUsdfAta.address, - userOtherToken = createUsdcAta.address, - ).instruction() - ) + // 7. Swap (USDC ATA -> USDF ATA) + when (pool) { + is FundSwapPool.Usdf -> { + add( + UsdfProgram_Swap( + amount = amount, + usdfToOther = false, + user = sender, + pool = pool.pool.address, + usdfVault = pool.pool.usdfVault, + otherVault = pool.pool.otherVault, + userUsdfToken = createUsdfAta.address, + userOtherToken = createUsdcAta.address, + ).instruction() + ) + } + is FundSwapPool.CoinbaseStableSwapper -> { + val swapAccounts = CoinbaseSwapAccounts.derive(Mint.usdc, Mint.usdf) + add( + CoinbaseStableSwapperProgram_Swap( + pool = swapAccounts.pool, + inVault = swapAccounts.inVault, + outVault = swapAccounts.outVault, + inVaultTokenAccount = swapAccounts.inVaultTokenAccount, + outVaultTokenAccount = swapAccounts.outVaultTokenAccount, + userFromTokenAccount = createUsdcAta.address, + toTokenAccount = createUsdfAta.address, + feeRecipientTokenAccount = swapAccounts.feeRecipientTokenAccount( + feeRecipient = pool.feeRecipient, + fromMint = Mint.usdc, + ), + feeRecipient = pool.feeRecipient, + fromMint = Mint.usdc, + toMint = Mint.usdf, + user = sender, + whitelist = swapAccounts.whitelist, + amountIn = amount, + minAmountOut = 0, + ).instruction() + ) + } + } // 8. Token::Transfer (USDF ATA -> USDF Swap PDA) add(