From 26f13f63d7fab4319eca99349c524168c576d0f9 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Sat, 25 Apr 2026 07:53:57 -0400 Subject: [PATCH] Add Coinbase stable swapper Solana program package --- .../accounts_liquidity_pool.go | 87 +++++++++++ solana/coinbasestableswapper/address.go | 58 ++++++++ .../instructions_swap.go | 139 ++++++++++++++++++ solana/coinbasestableswapper/program.go | 23 +++ .../types_account_type.go | 13 ++ .../types_instruction_type.go | 12 ++ solana/coinbasestableswapper/utils.go | 77 ++++++++++ 7 files changed, 409 insertions(+) create mode 100644 solana/coinbasestableswapper/accounts_liquidity_pool.go create mode 100644 solana/coinbasestableswapper/address.go create mode 100644 solana/coinbasestableswapper/instructions_swap.go create mode 100644 solana/coinbasestableswapper/program.go create mode 100644 solana/coinbasestableswapper/types_account_type.go create mode 100644 solana/coinbasestableswapper/types_instruction_type.go create mode 100644 solana/coinbasestableswapper/utils.go diff --git a/solana/coinbasestableswapper/accounts_liquidity_pool.go b/solana/coinbasestableswapper/accounts_liquidity_pool.go new file mode 100644 index 0000000..886de36 --- /dev/null +++ b/solana/coinbasestableswapper/accounts_liquidity_pool.go @@ -0,0 +1,87 @@ +package coinbase_stable_swapper + +import ( + "bytes" + "crypto/ed25519" + "encoding/binary" + "fmt" + + "github.com/mr-tron/base58" +) + +// Minimum size without the dynamic supported_tokens vector +const ( + LiquidityPoolAccountMinSize = (8 + // discriminator + 32 + // operations_authority + 32 + // pause_authority + 32 + // fee_recipient + 4 + // supported_tokens vector length (empty) + 8 + // fee_rate + 1 + // swaps_paused + 1 + // liquidity_paused + 1) // bump +) + +type LiquidityPoolAccount struct { + OperationsAuthority ed25519.PublicKey + PauseAuthority ed25519.PublicKey + FeeRecipient ed25519.PublicKey + SupportedTokens []ed25519.PublicKey + FeeRate uint64 + SwapsPaused bool + LiquidityPaused bool + Bump uint8 +} + +func (obj *LiquidityPoolAccount) Unmarshal(data []byte) error { + if len(data) < LiquidityPoolAccountMinSize { + return ErrInvalidAccountData + } + + var offset int + + var discriminator []byte + getDiscriminator(data, &discriminator, &offset) + if !bytes.Equal(discriminator, LiquidityPoolAccountDiscriminator) { + return ErrInvalidAccountData + } + + getKey(data, &obj.OperationsAuthority, &offset) + getKey(data, &obj.PauseAuthority, &offset) + getKey(data, &obj.FeeRecipient, &offset) + + // Read supported_tokens vector (4-byte length prefix + pubkeys) + vecLen := binary.LittleEndian.Uint32(data[offset:]) + offset += 4 + + obj.SupportedTokens = make([]ed25519.PublicKey, vecLen) + for i := uint32(0); i < vecLen; i++ { + getKey(data, &obj.SupportedTokens[i], &offset) + } + + getUint64(data, &obj.FeeRate, &offset) + getBool(data, &obj.SwapsPaused, &offset) + getBool(data, &obj.LiquidityPaused, &offset) + getUint8(data, &obj.Bump, &offset) + + return nil +} + +func (obj *LiquidityPoolAccount) String() string { + tokensList := make([]string, len(obj.SupportedTokens)) + for i, t := range obj.SupportedTokens { + tokensList[i] = base58.Encode(t) + } + + return fmt.Sprintf( + "LiquidityPool{operations_authority=%s,pause_authority=%s,fee_recipient=%s,supported_tokens=%v,fee_rate=%d,swaps_paused=%t,liquidity_paused=%t,bump=%d}", + base58.Encode(obj.OperationsAuthority), + base58.Encode(obj.PauseAuthority), + base58.Encode(obj.FeeRecipient), + tokensList, + obj.FeeRate, + obj.SwapsPaused, + obj.LiquidityPaused, + obj.Bump, + ) +} diff --git a/solana/coinbasestableswapper/address.go b/solana/coinbasestableswapper/address.go new file mode 100644 index 0000000..360afb9 --- /dev/null +++ b/solana/coinbasestableswapper/address.go @@ -0,0 +1,58 @@ +package coinbase_stable_swapper + +import ( + "crypto/ed25519" + + "github.com/code-payments/ocp-server/solana" +) + +var ( + LiquidityPoolPrefix = []byte("liquidity_pool") + TokenVaultPrefix = []byte("token_vault") + VaultTokenAccountPrefix = []byte("vault_token_account") + AddressWhitelistPrefix = []byte("address_whitelist") +) + +// GetPoolAddress returns the PDA for the liquidity pool +func GetPoolAddress() (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + LiquidityPoolPrefix, + ) +} + +type GetTokenVaultAddressArgs struct { + Pool ed25519.PublicKey + Mint ed25519.PublicKey +} + +// GetTokenVaultAddress returns the PDA for a token vault +func GetTokenVaultAddress(args *GetTokenVaultAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + TokenVaultPrefix, + args.Pool, + args.Mint, + ) +} + +type GetVaultTokenAccountAddressArgs struct { + Vault ed25519.PublicKey +} + +// GetVaultTokenAccountAddress returns the PDA for a vault's token account +func GetVaultTokenAccountAddress(args *GetVaultTokenAccountAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + VaultTokenAccountPrefix, + args.Vault, + ) +} + +// GetWhitelistAddress returns the PDA for the address whitelist +func GetWhitelistAddress() (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + AddressWhitelistPrefix, + ) +} diff --git a/solana/coinbasestableswapper/instructions_swap.go b/solana/coinbasestableswapper/instructions_swap.go new file mode 100644 index 0000000..f0cc5ce --- /dev/null +++ b/solana/coinbasestableswapper/instructions_swap.go @@ -0,0 +1,139 @@ +package coinbase_stable_swapper + +import ( + "crypto/ed25519" + + "github.com/code-payments/ocp-server/solana" +) + +const ( + SwapInstructionArgsSize = (8 + // discriminator + 8 + // amount_in + 8) // min_amount_out +) + +type SwapInstructionArgs struct { + AmountIn uint64 + MinAmountOut uint64 +} + +type SwapInstructionAccounts struct { + Pool ed25519.PublicKey + InVault ed25519.PublicKey + OutVault ed25519.PublicKey + InVaultTokenAccount ed25519.PublicKey + OutVaultTokenAccount ed25519.PublicKey + UserFromTokenAccount ed25519.PublicKey + ToTokenAccount ed25519.PublicKey + FeeRecipientTokenAccount ed25519.PublicKey + FeeRecipient ed25519.PublicKey + FromMint ed25519.PublicKey + ToMint ed25519.PublicKey + User ed25519.PublicKey + Whitelist ed25519.PublicKey +} + +func NewSwapInstruction( + accounts *SwapInstructionAccounts, + args *SwapInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, SwapInstructionArgsSize) + + putDiscriminator(data, SwapInstructionDiscriminator, &offset) + putUint64(data, args.AmountIn, &offset) + putUint64(data, args.MinAmountOut, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.Pool, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.InVault, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.OutVault, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.InVaultTokenAccount, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.OutVaultTokenAccount, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.UserFromTokenAccount, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.ToTokenAccount, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.FeeRecipientTokenAccount, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.FeeRecipient, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.FromMint, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.ToMint, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.User, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Whitelist, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: ASSOCIATED_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSTEM_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/solana/coinbasestableswapper/program.go b/solana/coinbasestableswapper/program.go new file mode 100644 index 0000000..9501297 --- /dev/null +++ b/solana/coinbasestableswapper/program.go @@ -0,0 +1,23 @@ +package coinbase_stable_swapper + +import ( + "crypto/ed25519" + "errors" +) + +var ( + ErrInvalidProgram = errors.New("invalid program id") + ErrInvalidAccountData = errors.New("unexpected account data") + ErrInvalidInstructionData = errors.New("unexpected instruction data") +) + +var ( + PROGRAM_ADDRESS = mustBase58Decode("pqgqKahpG1y2wsgxFhzaAnkV1cL9vk8MSg9qm4q646F") + PROGRAM_ID = ed25519.PublicKey(PROGRAM_ADDRESS) +) + +var ( + SPL_TOKEN_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) + ASSOCIATED_TOKEN_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")) + SYSTEM_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("11111111111111111111111111111111")) +) diff --git a/solana/coinbasestableswapper/types_account_type.go b/solana/coinbasestableswapper/types_account_type.go new file mode 100644 index 0000000..15d0ab4 --- /dev/null +++ b/solana/coinbasestableswapper/types_account_type.go @@ -0,0 +1,13 @@ +package coinbase_stable_swapper + +// Account discriminators from the on-chain IDL +var ( + // LiquidityPool discriminator: [66, 38, 17, 64, 188, 80, 68, 129] + LiquidityPoolAccountDiscriminator = []byte{66, 38, 17, 64, 188, 80, 68, 129} + + // AddressWhitelist discriminator: [10, 102, 46, 176, 154, 249, 160, 48] + AddressWhitelistAccountDiscriminator = []byte{10, 102, 46, 176, 154, 249, 160, 48} + + // TokenVault discriminator: [121, 7, 84, 254, 151, 228, 43, 144] + TokenVaultAccountDiscriminator = []byte{121, 7, 84, 254, 151, 228, 43, 144} +) diff --git a/solana/coinbasestableswapper/types_instruction_type.go b/solana/coinbasestableswapper/types_instruction_type.go new file mode 100644 index 0000000..b5e7bd3 --- /dev/null +++ b/solana/coinbasestableswapper/types_instruction_type.go @@ -0,0 +1,12 @@ +package coinbase_stable_swapper + +// Instruction discriminators from the on-chain IDL +var ( + // swap discriminator: [248, 198, 158, 145, 225, 117, 135, 200] + SwapInstructionDiscriminator = []byte{248, 198, 158, 145, 225, 117, 135, 200} +) + +func putDiscriminator(dst []byte, discriminator []byte, offset *int) { + copy(dst[*offset:], discriminator) + *offset += 8 +} diff --git a/solana/coinbasestableswapper/utils.go b/solana/coinbasestableswapper/utils.go new file mode 100644 index 0000000..01d1070 --- /dev/null +++ b/solana/coinbasestableswapper/utils.go @@ -0,0 +1,77 @@ +package coinbase_stable_swapper + +import ( + "crypto/ed25519" + "encoding/binary" + + "github.com/mr-tron/base58" +) + +func putKey(dst []byte, v ed25519.PublicKey, offset *int) { + copy(dst[*offset:], v) + *offset += ed25519.PublicKeySize +} + +func getKey(src []byte, dst *ed25519.PublicKey, offset *int) { + *dst = make([]byte, ed25519.PublicKeySize) + copy(*dst, src[*offset:]) + *offset += ed25519.PublicKeySize +} + +func putUint64(dst []byte, v uint64, offset *int) { + binary.LittleEndian.PutUint64(dst[*offset:], v) + *offset += 8 +} + +func getUint64(src []byte, dst *uint64, offset *int) { + *dst = binary.LittleEndian.Uint64(src[*offset:]) + *offset += 8 +} + +func putUint16(dst []byte, v uint16, offset *int) { + binary.LittleEndian.PutUint16(dst[*offset:], v) + *offset += 2 +} + +func getUint16(src []byte, dst *uint16, offset *int) { + *dst = binary.LittleEndian.Uint16(src[*offset:]) + *offset += 2 +} + +func putUint8(dst []byte, v uint8, offset *int) { + dst[*offset] = v + *offset += 1 +} + +func getUint8(src []byte, dst *uint8, offset *int) { + *dst = src[*offset] + *offset += 1 +} + +func getDiscriminator(src []byte, dst *[]byte, offset *int) { + *dst = make([]byte, 8) + copy(*dst, src[*offset:]) + *offset += 8 +} + +func putBool(dst []byte, v bool, offset *int) { + if v { + dst[*offset] = 1 + } else { + dst[*offset] = 0 + } + *offset += 1 +} + +func getBool(src []byte, dst *bool, offset *int) { + *dst = src[*offset] != 0 + *offset += 1 +} + +func mustBase58Decode(value string) []byte { + decoded, err := base58.Decode(value) + if err != nil { + panic(err) + } + return decoded +}