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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
4 changes: 4 additions & 0 deletions ocp/rpc/transaction/stateful_swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -1013,6 +1016,7 @@ func (s *transactionServer) handleStablecoinStatefulSwap(
initiateStablecoinSwapReq.SwapAmount,
initiateStablecoinSwapReq.FeeAmount,
selectedNonce,
coinbaseSwapVersion,
)

alts, err := swapHandler.GetAlts(ctx)
Expand Down
10 changes: 9 additions & 1 deletion ocp/rpc/transaction/stateless_swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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()}

Expand Down
27 changes: 23 additions & 4 deletions ocp/rpc/transaction/swap_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand All @@ -1224,6 +1236,7 @@ func NewCoinbaseStableSwapperSwapHandler(
swapAmount uint64,
feeAmount uint64,
selectedNonce *transaction_util.Nonce,
coinbaseSwapVersion transaction_util.CoinbaseSwapVersion,
) SwapHandler {
return &CoinbaseStableSwapperSwapHandler{
data: data,
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions ocp/rpc/transaction/swap_handler_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
98 changes: 74 additions & 24 deletions ocp/transaction/coinbase_swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
65 changes: 65 additions & 0 deletions ocp/transaction/coinbase_swap_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading