From 83d679ec6bc45ba14ee6aef1803473d2b7f76632 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 22 Oct 2026 09:13:43 -0400 Subject: [PATCH 1/2] Add support for swap v2 for the Coinbase Stableswapper --- .../instructions_swap_v2.go | 135 ++++++++++++++++++ .../instructions_swap_v2_test.go | 78 ++++++++++ .../types_instruction_type.go | 3 + 3 files changed, 216 insertions(+) create mode 100644 solana/coinbasestableswapper/instructions_swap_v2.go create mode 100644 solana/coinbasestableswapper/instructions_swap_v2_test.go diff --git a/solana/coinbasestableswapper/instructions_swap_v2.go b/solana/coinbasestableswapper/instructions_swap_v2.go new file mode 100644 index 0000000..d6ef012 --- /dev/null +++ b/solana/coinbasestableswapper/instructions_swap_v2.go @@ -0,0 +1,135 @@ +package coinbase_stable_swapper + +import ( + "crypto/ed25519" + + "github.com/code-payments/ocp-server/solana" +) + +const ( + SwapV2InstructionArgsSize = (8 + // discriminator + 8 + // amount_in + 8) // min_amount_out +) + +type SwapV2InstructionArgs struct { + AmountIn uint64 + MinAmountOut uint64 +} + +type SwapV2InstructionAccounts 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 +} + +// NewSwapV2Instruction builds a swap_v2 instruction, which is swap without the +// deprecated whitelist account. Args, logic, and effects are unchanged. +func NewSwapV2Instruction( + accounts *SwapV2InstructionAccounts, + args *SwapV2InstructionArgs, +) solana.Instruction { + var offset int + + // Serialize instruction arguments + data := make([]byte, SwapV2InstructionArgsSize) + + putDiscriminator(data, SwapV2InstructionDiscriminator, &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: 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/instructions_swap_v2_test.go b/solana/coinbasestableswapper/instructions_swap_v2_test.go new file mode 100644 index 0000000..a9b9c22 --- /dev/null +++ b/solana/coinbasestableswapper/instructions_swap_v2_test.go @@ -0,0 +1,78 @@ +package coinbase_stable_swapper + +import ( + "crypto/ed25519" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/code-payments/ocp-server/solana" +) + +func TestSwapV2Instruction_MatchesSwapWithoutWhitelist(t *testing.T) { + keys := make([]ed25519.PublicKey, 13) + for i := range keys { + keys[i] = make([]byte, ed25519.PublicKeySize) + keys[i][0] = byte(i + 1) + } + + v1 := NewSwapInstruction( + &SwapInstructionAccounts{ + 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], + Whitelist: keys[12], + }, + &SwapInstructionArgs{ + AmountIn: 1_500_000, + MinAmountOut: 1_490_000, + }, + ) + + v2 := NewSwapV2Instruction( + &SwapV2InstructionAccounts{ + 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], + }, + &SwapV2InstructionArgs{ + AmountIn: 1_500_000, + MinAmountOut: 1_490_000, + }, + ) + + require.EqualValues(t, PROGRAM_ADDRESS, v2.Program) + + require.Equal(t, []byte{ + 43, 4, 237, 11, 26, 201, 30, 98, // discriminator + 0x60, 0xe3, 0x16, 0, 0, 0, 0, 0, // amount_in = 1_500_000 + 0x50, 0xbc, 0x16, 0, 0, 0, 0, 0, // min_amount_out = 1_490_000 + }, v2.Data) + require.Equal(t, v1.Data[8:], v2.Data[8:]) + + // The whitelist is the 13th account in swap; swap_v2 drops it and keeps + // everything else in order. + require.Len(t, v1.Accounts, 16) + require.Len(t, v2.Accounts, 15) + require.EqualValues(t, keys[12], v1.Accounts[12].PublicKey) + expected := append(append([]solana.AccountMeta{}, v1.Accounts[:12]...), v1.Accounts[13:]...) + require.Equal(t, expected, v2.Accounts) +} diff --git a/solana/coinbasestableswapper/types_instruction_type.go b/solana/coinbasestableswapper/types_instruction_type.go index b5e7bd3..1dda9b9 100644 --- a/solana/coinbasestableswapper/types_instruction_type.go +++ b/solana/coinbasestableswapper/types_instruction_type.go @@ -4,6 +4,9 @@ package coinbase_stable_swapper var ( // swap discriminator: [248, 198, 158, 145, 225, 117, 135, 200] SwapInstructionDiscriminator = []byte{248, 198, 158, 145, 225, 117, 135, 200} + + // swap_v2 discriminator: [43, 4, 237, 11, 26, 201, 30, 98] + SwapV2InstructionDiscriminator = []byte{43, 4, 237, 11, 26, 201, 30, 98} ) func putDiscriminator(dst []byte, discriminator []byte, offset *int) { From e9143339aa698303056c15745ae99291ec9650a8 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Thu, 22 Oct 2026 10:04:48 -0400 Subject: [PATCH 2/2] Enable v2 swap at RPC handlers --- go.mod | 2 +- go.sum | 4 +- ocp/rpc/transaction/stateful_swap.go | 4 + ocp/rpc/transaction/stateless_swap.go | 10 ++- ocp/rpc/transaction/swap_handler.go | 27 ++++++- ocp/rpc/transaction/swap_handler_test.go | 17 ++++ ocp/transaction/coinbase_swap.go | 98 ++++++++++++++++++------ ocp/transaction/coinbase_swap_test.go | 65 ++++++++++++++++ 8 files changed, 195 insertions(+), 32 deletions(-) create mode 100644 ocp/rpc/transaction/swap_handler_test.go create mode 100644 ocp/transaction/coinbase_swap_test.go diff --git a/go.mod b/go.mod index 367d66d..b0f732f 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/dynamodb v1.59.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 github.com/code-payments/code-vm-indexer v1.2.0 - github.com/code-payments/ocp-protobuf-api v1.17.1-0.20261016120110-6f70c0d8e832 + github.com/code-payments/ocp-protobuf-api v1.18.1-0.20261022140124-98cf14de185d github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.3.3 github.com/golang/protobuf v1.5.4 diff --git a/go.sum b/go.sum index 3df4114..a65a3c2 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/code-payments/code-vm-indexer v1.2.0 h1:rSHpBMiT9BKgmKcXg/VIoi/h0t7jNxGx07Qz59m+6Q0= github.com/code-payments/code-vm-indexer v1.2.0/go.mod h1:vn91YN2qNqb+gGJeZe2+l+TNxVmEEiRHXXnIn2Y40h8= -github.com/code-payments/ocp-protobuf-api v1.17.1-0.20261016120110-6f70c0d8e832 h1:HFY6mGBaZm8C3tJDK2PvaVQjfUS91NFDDWQSzla80hA= -github.com/code-payments/ocp-protobuf-api v1.17.1-0.20261016120110-6f70c0d8e832/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= +github.com/code-payments/ocp-protobuf-api v1.18.1-0.20261022140124-98cf14de185d h1:IonLY5NnQdKH0Tpa/ZbzGbaUskhBmzij9IZiI7kudl4= +github.com/code-payments/ocp-protobuf-api v1.18.1-0.20261022140124-98cf14de185d/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/ocp/rpc/transaction/stateful_swap.go b/ocp/rpc/transaction/stateful_swap.go index d19189b..273d333 100644 --- a/ocp/rpc/transaction/stateful_swap.go +++ b/ocp/rpc/transaction/stateful_swap.go @@ -1003,6 +1003,9 @@ func (s *transactionServer) handleStablecoinStatefulSwap( selectedNonce.ReleaseIfNotReserved(ctx) }() + coinbaseSwapVersion := selectCoinbaseSwapVersion(initiateStablecoinSwapReq.SwapVersion) + log = log.With(zap.String("coinbase_swap_version", coinbaseSwapVersion.String())) + swapHandler := NewCoinbaseStableSwapperSwapHandler( s.data, owner, @@ -1013,6 +1016,7 @@ func (s *transactionServer) handleStablecoinStatefulSwap( initiateStablecoinSwapReq.SwapAmount, initiateStablecoinSwapReq.FeeAmount, selectedNonce, + coinbaseSwapVersion, ) alts, err := swapHandler.GetAlts(ctx) diff --git a/ocp/rpc/transaction/stateless_swap.go b/ocp/rpc/transaction/stateless_swap.go index fc464b6..3ea1743 100644 --- a/ocp/rpc/transaction/stateless_swap.go +++ b/ocp/rpc/transaction/stateless_swap.go @@ -234,7 +234,11 @@ func (s *transactionServer) handleStablecoinStatelessSwap( return handleStatelessSwapError(streamer, err) } - coinbaseSwapIxn := transaction_util.MakeCoinbaseSwapInstruction( + coinbaseSwapVersion := selectCoinbaseSwapVersion(initiateStablecoinSwapReq.SwapVersion) + log = log.With(zap.String("coinbase_swap_version", coinbaseSwapVersion.String())) + + coinbaseSwapIxn, err := transaction_util.MakeCoinbaseSwapInstruction( + coinbaseSwapVersion, coinbaseAccounts, owner.PublicKey().ToBytes(), fromMint.PublicKey().ToBytes(), @@ -244,6 +248,10 @@ func (s *transactionServer) handleStablecoinStatelessSwap( initiateStablecoinSwapReq.SwapAmount, initiateStablecoinSwapReq.SwapAmount, ) + if err != nil { + log.With(zap.Error(err)).Warn("failure building coinbase swap instruction") + return handleStatelessSwapError(streamer, err) + } alts := []solana.AddressLookupTable{transaction_util.GetAltForCoreMint()} diff --git a/ocp/rpc/transaction/swap_handler.go b/ocp/rpc/transaction/swap_handler.go index a7df954..598508e 100644 --- a/ocp/rpc/transaction/swap_handler.go +++ b/ocp/rpc/transaction/swap_handler.go @@ -1193,6 +1193,17 @@ func (h *ReserveTreasuryFundedCreateAndBuySwapHandler) MakeInstructions(ctx cont }, nil } +// selectCoinbaseSwapVersion picks the CoinbaseStableSwapper swap instruction +// version from the client's requested swap_version. Clients rebuild and sign +// the transaction locally, so the server must match what the client builds. +// Only an explicit 2 selects v2. Older clients leave it unset and build v1. +func selectCoinbaseSwapVersion(requested uint32) transaction_util.CoinbaseSwapVersion { + if requested == 2 { + return transaction_util.CoinbaseSwapV2 + } + return transaction_util.CoinbaseSwapV1 +} + type CoinbaseStableSwapperSwapHandler struct { data ocp_data.Provider @@ -1210,8 +1221,9 @@ type CoinbaseStableSwapperSwapHandler struct { computeUnitPrice uint64 memoValue string - feeDestination *common.Account - coinbaseAccounts *transaction_util.CoinbaseSwapAccounts + feeDestination *common.Account + coinbaseSwapVersion transaction_util.CoinbaseSwapVersion + coinbaseAccounts *transaction_util.CoinbaseSwapAccounts } func NewCoinbaseStableSwapperSwapHandler( @@ -1224,6 +1236,7 @@ func NewCoinbaseStableSwapperSwapHandler( swapAmount uint64, feeAmount uint64, selectedNonce *transaction_util.Nonce, + coinbaseSwapVersion transaction_util.CoinbaseSwapVersion, ) SwapHandler { return &CoinbaseStableSwapperSwapHandler{ data: data, @@ -1240,7 +1253,9 @@ func NewCoinbaseStableSwapperSwapHandler( computeUnitLimit: 200_000, computeUnitPrice: 10_000, memoValue: "coinbase_stable_swapper_v0", - feeDestination: common.CoreMintFeesAccount, + + feeDestination: common.CoreMintFeesAccount, + coinbaseSwapVersion: coinbaseSwapVersion, } } @@ -1325,7 +1340,8 @@ func (h *CoinbaseStableSwapperSwapHandler) MakeInstructions(ctx context.Context) }, ) - coinbaseSwapIxn := transaction_util.MakeCoinbaseSwapInstruction( + coinbaseSwapIxn, err := transaction_util.MakeCoinbaseSwapInstruction( + h.coinbaseSwapVersion, coinbaseAccounts, h.swapAuthority.PublicKey().ToBytes(), h.fromMint.PublicKey().ToBytes(), @@ -1335,6 +1351,9 @@ func (h *CoinbaseStableSwapperSwapHandler) MakeInstructions(ctx context.Context) h.swapAmount, h.swapAmount, ) + if err != nil { + return nil, err + } closeSwapAuthorityFromMintAtaIxn := token.CloseAccount( swapAuthorityFromMintAta, diff --git a/ocp/rpc/transaction/swap_handler_test.go b/ocp/rpc/transaction/swap_handler_test.go new file mode 100644 index 0000000..febe7d4 --- /dev/null +++ b/ocp/rpc/transaction/swap_handler_test.go @@ -0,0 +1,17 @@ +package transaction + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + transaction_util "github.com/code-payments/ocp-server/ocp/transaction" +) + +func TestSelectCoinbaseSwapVersion(t *testing.T) { + // Unset (older clients) and 1 both use the legacy swap instruction + assert.Equal(t, transaction_util.CoinbaseSwapV1, selectCoinbaseSwapVersion(0)) + assert.Equal(t, transaction_util.CoinbaseSwapV1, selectCoinbaseSwapVersion(1)) + + assert.Equal(t, transaction_util.CoinbaseSwapV2, selectCoinbaseSwapVersion(2)) +} diff --git a/ocp/transaction/coinbase_swap.go b/ocp/transaction/coinbase_swap.go index f68a73e..c1d15f6 100644 --- a/ocp/transaction/coinbase_swap.go +++ b/ocp/transaction/coinbase_swap.go @@ -13,9 +13,31 @@ import ( "github.com/code-payments/ocp-server/solana/token" ) +// CoinbaseSwapVersion selects the CoinbaseStableSwapper swap instruction. +// Clients build and sign swap transactions locally, so the server must build +// the same version as the client. +type CoinbaseSwapVersion uint8 + +const ( + CoinbaseSwapV1 CoinbaseSwapVersion = iota + 1 // swap, with the deprecated whitelist account + CoinbaseSwapV2 // swap_v2 +) + +func (v CoinbaseSwapVersion) String() string { + switch v { + case CoinbaseSwapV1: + return "v1" + case CoinbaseSwapV2: + return "v2" + default: + return "unknown" + } +} + // CoinbaseSwapAccounts captures all derived PDAs and on-chain values needed -// to build a CoinbaseStableSwapper::Swap instruction for a (fromMint, toMint) +// to build a CoinbaseStableSwapper swap instruction for a (fromMint, toMint) // pair. FeeRecipient is sourced from the on-chain LiquidityPool account. +// Whitelist is only used by CoinbaseSwapV1. type CoinbaseSwapAccounts struct { Pool ed25519.PublicKey InVault ed25519.PublicKey @@ -133,33 +155,61 @@ func GetCoinbaseSwapDestinationLiquidity(ctx context.Context, data ocp_data.Prov return info.Amount, nil } -// MakeCoinbaseSwapInstruction builds a CoinbaseStableSwapper::Swap instruction. +// MakeCoinbaseSwapInstruction builds a CoinbaseStableSwapper swap instruction +// for the provided version. func MakeCoinbaseSwapInstruction( + version CoinbaseSwapVersion, accounts *CoinbaseSwapAccounts, user ed25519.PublicKey, fromMint, toMint ed25519.PublicKey, userFromTokenAccount, userToTokenAccount ed25519.PublicKey, amountIn, minAmountOut uint64, -) solana.Instruction { - return coinbase_stable_swapper.NewSwapInstruction( - &coinbase_stable_swapper.SwapInstructionAccounts{ - Pool: accounts.Pool, - InVault: accounts.InVault, - OutVault: accounts.OutVault, - InVaultTokenAccount: accounts.InVaultTokenAccount, - OutVaultTokenAccount: accounts.OutVaultTokenAccount, - UserFromTokenAccount: userFromTokenAccount, - ToTokenAccount: userToTokenAccount, - FeeRecipientTokenAccount: accounts.FeeRecipientTokenAccount, - FeeRecipient: accounts.FeeRecipient, - FromMint: fromMint, - ToMint: toMint, - User: user, - Whitelist: accounts.Whitelist, - }, - &coinbase_stable_swapper.SwapInstructionArgs{ - AmountIn: amountIn, - MinAmountOut: minAmountOut, - }, - ) +) (solana.Instruction, error) { + switch version { + case CoinbaseSwapV1: + return coinbase_stable_swapper.NewSwapInstruction( + &coinbase_stable_swapper.SwapInstructionAccounts{ + Pool: accounts.Pool, + InVault: accounts.InVault, + OutVault: accounts.OutVault, + InVaultTokenAccount: accounts.InVaultTokenAccount, + OutVaultTokenAccount: accounts.OutVaultTokenAccount, + UserFromTokenAccount: userFromTokenAccount, + ToTokenAccount: userToTokenAccount, + FeeRecipientTokenAccount: accounts.FeeRecipientTokenAccount, + FeeRecipient: accounts.FeeRecipient, + FromMint: fromMint, + ToMint: toMint, + User: user, + Whitelist: accounts.Whitelist, + }, + &coinbase_stable_swapper.SwapInstructionArgs{ + AmountIn: amountIn, + MinAmountOut: minAmountOut, + }, + ), nil + case CoinbaseSwapV2: + return coinbase_stable_swapper.NewSwapV2Instruction( + &coinbase_stable_swapper.SwapV2InstructionAccounts{ + Pool: accounts.Pool, + InVault: accounts.InVault, + OutVault: accounts.OutVault, + InVaultTokenAccount: accounts.InVaultTokenAccount, + OutVaultTokenAccount: accounts.OutVaultTokenAccount, + UserFromTokenAccount: userFromTokenAccount, + ToTokenAccount: userToTokenAccount, + FeeRecipientTokenAccount: accounts.FeeRecipientTokenAccount, + FeeRecipient: accounts.FeeRecipient, + FromMint: fromMint, + ToMint: toMint, + User: user, + }, + &coinbase_stable_swapper.SwapV2InstructionArgs{ + AmountIn: amountIn, + MinAmountOut: minAmountOut, + }, + ), nil + default: + return solana.Instruction{}, errors.Errorf("unsupported coinbase swap version: %d", version) + } } diff --git a/ocp/transaction/coinbase_swap_test.go b/ocp/transaction/coinbase_swap_test.go new file mode 100644 index 0000000..9053094 --- /dev/null +++ b/ocp/transaction/coinbase_swap_test.go @@ -0,0 +1,65 @@ +package transaction + +import ( + "crypto/ed25519" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + coinbase_stable_swapper "github.com/code-payments/ocp-server/solana/coinbasestableswapper" +) + +func TestMakeCoinbaseSwapInstruction(t *testing.T) { + keys := make([]ed25519.PublicKey, 14) + for i := range keys { + keys[i] = make([]byte, ed25519.PublicKeySize) + keys[i][0] = byte(i + 1) + } + + accounts := &CoinbaseSwapAccounts{ + Pool: keys[0], + InVault: keys[1], + OutVault: keys[2], + InVaultTokenAccount: keys[3], + OutVaultTokenAccount: keys[4], + Whitelist: keys[5], + FeeRecipient: keys[6], + FeeRecipientTokenAccount: keys[7], + } + user, fromMint, toMint, userFrom, userTo := keys[8], keys[9], keys[10], keys[11], keys[12] + + build := func(version CoinbaseSwapVersion) ([]ed25519.PublicKey, []byte, error) { + ixn, err := MakeCoinbaseSwapInstruction(version, accounts, user, fromMint, toMint, userFrom, userTo, 1_000_000, 990_000) + if err != nil { + return nil, nil, err + } + assert.EqualValues(t, coinbase_stable_swapper.PROGRAM_ADDRESS, ixn.Program) + var ixnKeys []ed25519.PublicKey + for _, meta := range ixn.Accounts { + ixnKeys = append(ixnKeys, meta.PublicKey) + } + return ixnKeys, ixn.Data, nil + } + + v1Keys, v1Data, err := build(CoinbaseSwapV1) + require.NoError(t, err) + assert.Equal(t, coinbase_stable_swapper.SwapInstructionDiscriminator, v1Data[:8]) + assert.Len(t, v1Keys, 16) + assert.Contains(t, v1Keys, accounts.Whitelist) + + v2Keys, v2Data, err := build(CoinbaseSwapV2) + require.NoError(t, err) + assert.Equal(t, coinbase_stable_swapper.SwapV2InstructionDiscriminator, v2Data[:8]) + assert.Len(t, v2Keys, 15) + assert.NotContains(t, v2Keys, accounts.Whitelist) + + // Same args and same accounts, other than the whitelist + assert.Equal(t, v1Data[8:], v2Data[8:]) + assert.Equal(t, append(append([]ed25519.PublicKey{}, v1Keys[:12]...), v1Keys[13:]...), v2Keys) + + for _, version := range []CoinbaseSwapVersion{0, CoinbaseSwapV2 + 1} { + _, _, err = build(version) + assert.Error(t, err) + } +}