From 6d4b3747bffaf6f984e6e51b7f4bf2472bb496c1 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Tue, 16 Dec 2025 11:54:59 -0500 Subject: [PATCH] Add USDF swap program support --- solana/usdfswap/accounts_pool.go | 87 ++++++++++++++++++ solana/usdfswap/address.go | 44 +++++++++ solana/usdfswap/instructions_initialize.go | 102 +++++++++++++++++++++ solana/usdfswap/instructions_swap.go | 86 +++++++++++++++++ solana/usdfswap/instructions_transfer.go | 74 +++++++++++++++ solana/usdfswap/program.go | 29 ++++++ solana/usdfswap/types_account_type.go | 8 ++ solana/usdfswap/types_instruction_type.go | 16 ++++ solana/usdfswap/utils.go | 83 +++++++++++++++++ 9 files changed, 529 insertions(+) create mode 100644 solana/usdfswap/accounts_pool.go create mode 100644 solana/usdfswap/address.go create mode 100644 solana/usdfswap/instructions_initialize.go create mode 100644 solana/usdfswap/instructions_swap.go create mode 100644 solana/usdfswap/instructions_transfer.go create mode 100644 solana/usdfswap/program.go create mode 100644 solana/usdfswap/types_account_type.go create mode 100644 solana/usdfswap/types_instruction_type.go create mode 100644 solana/usdfswap/utils.go diff --git a/solana/usdfswap/accounts_pool.go b/solana/usdfswap/accounts_pool.go new file mode 100644 index 0000000..f995521 --- /dev/null +++ b/solana/usdfswap/accounts_pool.go @@ -0,0 +1,87 @@ +package usdf_swap + +import ( + "bytes" + "crypto/ed25519" + "fmt" + + "github.com/mr-tron/base58" +) + +const ( + PoolAccountSize = (8 + // discriminator + 32 + // authority + MaxPoolNameLength + // name + 32 + // usdf_mint + 32 + // other_mint + 32 + // usdf_vault + 32 + // other_vault + 1 + // bump + 1 + // usdf_vault_bump + 1 + // other_vault_bump + 1 + // usdf_decimals + 1 + // other_decimals + 3) // padding +) + +var PoolAccountDiscriminator = []byte{byte(AccountTypePool), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} + +type PoolAccount struct { + Authority ed25519.PublicKey + Name string + UsdfMint ed25519.PublicKey + OtherMint ed25519.PublicKey + UsdfVault ed25519.PublicKey + OtherVault ed25519.PublicKey + Bump uint8 + UsdfVaultBump uint8 + OtherVaultBump uint8 + UsdfDecimals uint8 + OtherDecimals uint8 +} + +func (obj *PoolAccount) Unmarshal(data []byte) error { + if len(data) < PoolAccountSize { + return ErrInvalidAccountData + } + + var offset int + + var discriminator []byte + getDiscriminator(data, &discriminator, &offset) + if !bytes.Equal(discriminator, PoolAccountDiscriminator) { + return ErrInvalidAccountData + } + + getKey(data, &obj.Authority, &offset) + getFixedString(data, &obj.Name, MaxPoolNameLength, &offset) + getKey(data, &obj.UsdfMint, &offset) + getKey(data, &obj.OtherMint, &offset) + getKey(data, &obj.UsdfVault, &offset) + getKey(data, &obj.OtherVault, &offset) + getUint8(data, &obj.Bump, &offset) + getUint8(data, &obj.UsdfVaultBump, &offset) + getUint8(data, &obj.OtherVaultBump, &offset) + getUint8(data, &obj.UsdfDecimals, &offset) + getUint8(data, &obj.OtherDecimals, &offset) + offset += 3 // padding + + return nil +} + +func (obj *PoolAccount) String() string { + return fmt.Sprintf( + "Pool{authority=%s,name=%s,usdf_mint=%s,other_mint=%s,usdf_vault=%s,other_vault=%s,bump=%d,usdf_vault_bump=%d,other_vault_bump=%d,usdf_decimals=%d,other_decimals=%d}", + base58.Encode(obj.Authority), + obj.Name, + base58.Encode(obj.UsdfMint), + base58.Encode(obj.OtherMint), + base58.Encode(obj.UsdfVault), + base58.Encode(obj.OtherVault), + obj.Bump, + obj.UsdfVaultBump, + obj.OtherVaultBump, + obj.UsdfDecimals, + obj.OtherDecimals, + ) +} diff --git a/solana/usdfswap/address.go b/solana/usdfswap/address.go new file mode 100644 index 0000000..8885b72 --- /dev/null +++ b/solana/usdfswap/address.go @@ -0,0 +1,44 @@ +package usdf_swap + +import ( + "crypto/ed25519" + + "github.com/code-payments/ocp-server/solana" +) + +var ( + PoolPrefix = []byte("pool") + VaultPrefix = []byte("vault") +) + +type GetPoolAddressArgs struct { + Authority ed25519.PublicKey + Name string + UsdfMint ed25519.PublicKey + OtherMint ed25519.PublicKey +} + +func GetPoolAddress(args *GetPoolAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + PoolPrefix, + args.Authority, + []byte(toFixedString(args.Name, MaxPoolNameLength)), + args.UsdfMint, + args.OtherMint, + ) +} + +type GetVaultAddressArgs struct { + Pool ed25519.PublicKey + Mint ed25519.PublicKey +} + +func GetVaultAddress(args *GetVaultAddressArgs) (ed25519.PublicKey, uint8, error) { + return solana.FindProgramAddressAndBump( + PROGRAM_ID, + VaultPrefix, + args.Pool, + args.Mint, + ) +} diff --git a/solana/usdfswap/instructions_initialize.go b/solana/usdfswap/instructions_initialize.go new file mode 100644 index 0000000..d5aad0a --- /dev/null +++ b/solana/usdfswap/instructions_initialize.go @@ -0,0 +1,102 @@ +package usdf_swap + +import ( + "crypto/ed25519" + + "github.com/code-payments/ocp-server/solana" +) + +const ( + InitializeInstructionArgsSize = (MaxPoolNameLength + // name + 1 + // bump + 1 + // usdf_vault_bump + 1) // other_vault_bump +) + +type InitializeInstructionArgs struct { + Name string + Bump uint8 + UsdfVaultBump uint8 + OtherVaultBump uint8 +} + +type InitializeInstructionAccounts struct { + Authority ed25519.PublicKey + UsdfMint ed25519.PublicKey + OtherMint ed25519.PublicKey + Pool ed25519.PublicKey + UsdfVault ed25519.PublicKey + OtherVault ed25519.PublicKey +} + +func NewInitializeInstruction( + accounts *InitializeInstructionAccounts, + args *InitializeInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, 1+InitializeInstructionArgsSize) + + putInstructionType(data, InstructionTypeInitialize, &offset) + putFixedString(data, args.Name, MaxPoolNameLength, &offset) + putUint8(data, args.Bump, &offset) + putUint8(data, args.UsdfVaultBump, &offset) + putUint8(data, args.OtherVaultBump, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.Authority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.UsdfMint, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.OtherMint, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.Pool, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.UsdfVault, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.OtherVault, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSTEM_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: SYSVAR_RENT_PUBKEY, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/solana/usdfswap/instructions_swap.go b/solana/usdfswap/instructions_swap.go new file mode 100644 index 0000000..ee05d33 --- /dev/null +++ b/solana/usdfswap/instructions_swap.go @@ -0,0 +1,86 @@ +package usdf_swap + +import ( + "crypto/ed25519" + + "github.com/code-payments/ocp-server/solana" +) + +const ( + SwapInstructionArgsSize = (8 + // amount + 1) // usdf_to_other +) + +type SwapInstructionArgs struct { + Amount uint64 + UsdfToOther bool +} + +type SwapInstructionAccounts struct { + User ed25519.PublicKey + Pool ed25519.PublicKey + UsdfVault ed25519.PublicKey + OtherVault ed25519.PublicKey + UserUsdfToken ed25519.PublicKey + UserOtherToken ed25519.PublicKey +} + +func NewSwapInstruction( + accounts *SwapInstructionAccounts, + args *SwapInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, 1+SwapInstructionArgsSize) + + putInstructionType(data, InstructionTypeSwap, &offset) + putUint64(data, args.Amount, &offset) + putBool(data, args.UsdfToOther, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.User, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Pool, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.UsdfVault, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.OtherVault, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.UserUsdfToken, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.UserOtherToken, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/solana/usdfswap/instructions_transfer.go b/solana/usdfswap/instructions_transfer.go new file mode 100644 index 0000000..36afdaa --- /dev/null +++ b/solana/usdfswap/instructions_transfer.go @@ -0,0 +1,74 @@ +package usdf_swap + +import ( + "crypto/ed25519" + + "github.com/code-payments/ocp-server/solana" +) + +const ( + TransferInstructionArgsSize = (8 + // amount + 1) // is_usdf +) + +type TransferInstructionArgs struct { + Amount uint64 + IsUsdf bool +} + +type TransferInstructionAccounts struct { + Authority ed25519.PublicKey + Pool ed25519.PublicKey + Vault ed25519.PublicKey + Destination ed25519.PublicKey +} + +func NewTransferInstruction( + accounts *TransferInstructionAccounts, + args *TransferInstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, 1+TransferInstructionArgsSize) + + putInstructionType(data, InstructionTypeTransfer, &offset) + putUint64(data, args.Amount, &offset) + putBool(data, args.IsUsdf, &offset) + + return solana.Instruction{ + Program: PROGRAM_ADDRESS, + + // Instruction args + Data: data, + + // Instruction accounts + Accounts: []solana.AccountMeta{ + { + PublicKey: accounts.Authority, + IsWritable: true, + IsSigner: true, + }, + { + PublicKey: accounts.Pool, + IsWritable: false, + IsSigner: false, + }, + { + PublicKey: accounts.Vault, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: accounts.Destination, + IsWritable: true, + IsSigner: false, + }, + { + PublicKey: SPL_TOKEN_PROGRAM_ID, + IsWritable: false, + IsSigner: false, + }, + }, + } +} diff --git a/solana/usdfswap/program.go b/solana/usdfswap/program.go new file mode 100644 index 0000000..5da3edd --- /dev/null +++ b/solana/usdfswap/program.go @@ -0,0 +1,29 @@ +package usdf_swap + +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("usdfcP2V1bh1Lz7Y87pxR4zJd3wnVtssJ6GeSHFeZeu") + PROGRAM_ID = ed25519.PublicKey(PROGRAM_ADDRESS) +) + +var ( + SPL_TOKEN_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")) + SYSTEM_PROGRAM_ID = ed25519.PublicKey(mustBase58Decode("11111111111111111111111111111111")) + + SYSVAR_RENT_PUBKEY = ed25519.PublicKey(mustBase58Decode("SysvarRent111111111111111111111111111111111")) +) + +const ( + MaxPoolNameLength = 32 + MaxSwapDollars = 1000 +) diff --git a/solana/usdfswap/types_account_type.go b/solana/usdfswap/types_account_type.go new file mode 100644 index 0000000..0c499d3 --- /dev/null +++ b/solana/usdfswap/types_account_type.go @@ -0,0 +1,8 @@ +package usdf_swap + +type AccountType uint8 + +const ( + AccountTypeUnknown AccountType = iota + AccountTypePool +) diff --git a/solana/usdfswap/types_instruction_type.go b/solana/usdfswap/types_instruction_type.go new file mode 100644 index 0000000..5154cf0 --- /dev/null +++ b/solana/usdfswap/types_instruction_type.go @@ -0,0 +1,16 @@ +package usdf_swap + +type InstructionType uint8 + +const ( + InstructionTypeUnknown InstructionType = iota + + InstructionTypeInitialize + InstructionTypeSwap + InstructionTypeTransfer +) + +func putInstructionType(dst []byte, v InstructionType, offset *int) { + dst[*offset] = uint8(v) + *offset += 1 +} diff --git a/solana/usdfswap/utils.go b/solana/usdfswap/utils.go new file mode 100644 index 0000000..a7e092d --- /dev/null +++ b/solana/usdfswap/utils.go @@ -0,0 +1,83 @@ +package usdf_swap + +import ( + "crypto/ed25519" + "encoding/binary" + "strings" + + "github.com/mr-tron/base58" +) + +func getDiscriminator(src []byte, dst *[]byte, offset *int) { + *dst = make([]byte, 8) + copy(*dst, src[*offset:]) + *offset += 8 +} + +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 putFixedString(dst []byte, v string, length int, offset *int) { + copy(dst[*offset:], toFixedString(v, length)) + *offset += length +} +func getFixedString(data []byte, dst *string, length int, offset *int) { + *dst = string(data[*offset : *offset+length]) + *dst = removeFixedStringPadding(*dst) + *offset += length +} + +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 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 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 toFixedString(value string, length int) string { + fixed := make([]byte, length) + copy(fixed, []byte(value)) + return string(fixed) +} +func removeFixedStringPadding(value string) string { + return strings.TrimRight(value, string([]byte{0})) +} + +func mustBase58Decode(value string) []byte { + decoded, err := base58.Decode(value) + if err != nil { + panic(err) + } + return decoded +}