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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -27,13 +29,15 @@ 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 @@ -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 {
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ 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 @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ internal fun buildUsdcToUsdfSwapInstructions(
fromMint = Mint.usdc,
toMint = Mint.usdf,
user = sender,
whitelist = swapAccounts.whitelist,
amountIn = amount,
minAmountOut = 0,
).instruction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ---
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -114,28 +114,28 @@ 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)
}

@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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading
Loading