Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -29,15 +27,13 @@ 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,
inVault = inVault,
outVault = outVault,
inVaultTokenAccount = inVaultTokenAccount,
outVaultTokenAccount = outVaultTokenAccount,
whitelist = whitelist,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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),
Expand All @@ -48,16 +50,14 @@ internal class CoinbaseStableSwapperProgram_Swap(
}

override fun encode(): List<Byte> = buildList {
// 8-byte Anchor discriminator: [248, 198, 158, 145, 225, 117, 135, 200]
addAll(DISCRIMINATOR.toList())
addAll(amountIn.bytes)
addAll(minAmountOut.bytes)
}

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,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -130,7 +130,6 @@ internal fun buildStablecoinSwapperInstructions(
fromMint = fromMintMetadata.address,
toMint = toMintMetadata.address,
user = swapAuthority,
whitelist = swapAccounts.whitelist,
amountIn = amount,
minAmountOut = minOutput,
).instruction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ---
Expand Down
Loading
Loading