Files
pump-parser/tx_binary_test.go

904 lines
32 KiB
Go
Raw Permalink Normal View History

2026-04-16 16:40:21 +08:00
package pump_parser
import (
"bytes"
"io"
"testing"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func TestTxBinaryRoundTrip(t *testing.T) {
txHash := [64]byte{}
for i := range txHash {
txHash[i] = byte(i + 1)
}
original := &Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 123456789,
BlockIndex: 42,
TxHash: &txHash,
CuFee: decimal.NewFromInt(5000),
CUPrice: decimal.RequireFromString("0.123456"),
BeforeSolBalance: decimal.RequireFromString("1.500000000"),
AfterSOLBalance: decimal.RequireFromString("1.234567890"),
ComputeUnitsConsumed: 345678,
CuLimit: 400000,
Platform: map[string]platformInfo{
PlatformGMGN: {
Platform: PlatformGMGN,
PlatformFee: decimal.RequireFromString("0.010000000"),
},
PlatformPhoton: {
Platform: PlatformPhoton,
PlatformFee: decimal.RequireFromString("0.020000000"),
},
},
MevAgent: map[string]mevInfo{
MevAgentJito: {
MevAgent: MevAgentJito,
MevAgentFee: decimal.RequireFromString("0.030000000"),
},
},
Swaps: []Swap{
{
Program: SolProgramPump,
Event: TxEventBuy,
TxIndex: 7,
InstrIdx: 2,
InnerIdx: 1,
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
BaseAmount: decimal.NewFromInt(1200),
QuoteAmount: decimal.NewFromInt(3400),
SwapMode: SwapModeExactIn,
FixedAmount: decimal.NewFromInt(3400),
FixedAmountSide: SwapAmountSideQuote,
FixedMint: solana.WrappedSol,
LimitAmountType: SwapLimitTypeMinOut,
LimitAmount: decimal.NewFromInt(1000),
LimitAmountSide: SwapAmountSideBase,
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
ActualLimitAmount: decimal.NewFromInt(1200),
ActualLimitAmountSide: SwapAmountSideBase,
SlippageBps: decimal.RequireFromString("833.3333"),
BaseReserve: decimal.NewFromInt(5555),
QuoteReserve: decimal.NewFromInt(9999),
Mayhem: true,
Cashback: false,
UserBaseBalance: decimal.NewFromInt(777),
UserQuoteBalance: decimal.NewFromInt(888),
EntryContract: mustPubKey("ComputeBudget111111111111111111111111111111"),
MigrateToPool: mustPubKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
MigrateTopProgram: mustPubKey("AddressLookupTab1e1111111111111111111111111"),
LpMint: mustPubKey("4Nd1mJf8JQhRVTfJxW2YxXLNQKhPYo1JzN1u2KAPY1Hn"),
AfterSOLBalance: decimal.RequireFromString("0.321000000"),
ActiveBinId: 11,
FeeAmount: decimal.NewFromInt(99),
FeeBps: "123",
FeeSide: "base",
ConsumeUnit: 9999,
},
},
}
encoded, err := EncodeTxBinary(original)
if err != nil {
t.Fatalf("EncodeTxBinary() error = %v", err)
}
decoded, err := DecodeTxBinary(encoded)
if err != nil {
t.Fatalf("DecodeTxBinary() error = %v", err)
}
if decoded.Signer != original.Signer {
t.Fatalf("Signer = %s, want %s", decoded.Signer, original.Signer)
}
if decoded.Block != original.Block {
t.Fatalf("Block = %d, want %d", decoded.Block, original.Block)
}
if decoded.BlockIndex != original.BlockIndex {
t.Fatalf("BlockIndex = %d, want %d", decoded.BlockIndex, original.BlockIndex)
}
if decoded.TxHash == nil {
t.Fatal("TxHash = nil, want non-nil")
}
if *decoded.TxHash != *original.TxHash {
t.Fatalf("TxHash mismatch")
}
if !decoded.CuFee.Equal(original.CuFee) {
t.Fatalf("CuFee = %s, want %s", decoded.CuFee, original.CuFee)
}
if !decoded.CUPrice.Equal(original.CUPrice) {
t.Fatalf("CUPrice = %s, want %s", decoded.CUPrice, original.CUPrice)
}
if decoded.BeforeSolBalance.StringFixed(9) != original.BeforeSolBalance.StringFixed(9) {
t.Fatalf("BeforeSolBalance = %s, want %s", decoded.BeforeSolBalance, original.BeforeSolBalance)
}
if decoded.AfterSOLBalance.StringFixed(9) != original.AfterSOLBalance.StringFixed(9) {
t.Fatalf("AfterSOLBalance = %s, want %s", decoded.AfterSOLBalance, original.AfterSOLBalance)
}
if decoded.CuLimit != original.CuLimit {
t.Fatalf("CuLimit = %d, want %d", decoded.CuLimit, original.CuLimit)
}
if decoded.ComputeUnitsConsumed != original.ComputeUnitsConsumed {
t.Fatalf("ComputeUnitsConsumed = %d, want %d", decoded.ComputeUnitsConsumed, original.ComputeUnitsConsumed)
}
if len(decoded.Platform) != len(original.Platform) {
t.Fatalf("Platform len = %d, want %d", len(decoded.Platform), len(original.Platform))
}
if !decoded.Platform[PlatformGMGN].PlatformFee.Equal(original.Platform[PlatformGMGN].PlatformFee) {
t.Fatalf("Platform fee mismatch")
}
if len(decoded.MevAgent) != len(original.MevAgent) {
t.Fatalf("MevAgent len = %d, want %d", len(decoded.MevAgent), len(original.MevAgent))
}
if !decoded.MevAgent[MevAgentJito].MevAgentFee.Equal(original.MevAgent[MevAgentJito].MevAgentFee) {
t.Fatalf("MevAgent fee mismatch")
}
if len(decoded.Swaps) != 1 {
t.Fatalf("Swaps len = %d, want 1", len(decoded.Swaps))
}
swap := decoded.Swaps[0]
if swap.Program != original.Swaps[0].Program {
t.Fatalf("swap.Program = %s, want %s", swap.Program, original.Swaps[0].Program)
}
if swap.Event != original.Swaps[0].Event {
t.Fatalf("swap.Event = %s, want %s", swap.Event, original.Swaps[0].Event)
}
if swap.TxIndex != original.Swaps[0].TxIndex {
t.Fatalf("swap.TxIndex = %d, want %d", swap.TxIndex, original.Swaps[0].TxIndex)
}
if !swap.BaseAmount.Equal(original.Swaps[0].BaseAmount) {
t.Fatalf("swap.BaseAmount = %s, want %s", swap.BaseAmount, original.Swaps[0].BaseAmount)
}
if !swap.QuoteAmount.Equal(original.Swaps[0].QuoteAmount) {
t.Fatalf("swap.QuoteAmount = %s, want %s", swap.QuoteAmount, original.Swaps[0].QuoteAmount)
}
if !swap.FixedAmount.Equal(original.Swaps[0].FixedAmount) {
t.Fatalf("swap.FixedAmount = %s, want %s", swap.FixedAmount, original.Swaps[0].FixedAmount)
}
if !swap.LimitAmount.Equal(original.Swaps[0].LimitAmount) {
t.Fatalf("swap.LimitAmount = %s, want %s", swap.LimitAmount, original.Swaps[0].LimitAmount)
}
if !swap.ActualLimitAmount.Equal(original.Swaps[0].ActualLimitAmount) {
t.Fatalf("swap.ActualLimitAmount = %s, want %s", swap.ActualLimitAmount, original.Swaps[0].ActualLimitAmount)
}
if swap.SlippageBps.String() != "833" {
t.Fatalf("swap.SlippageBps = %s, want 833", swap.SlippageBps)
}
if !swap.BaseReserve.Equal(original.Swaps[0].BaseReserve) {
t.Fatalf("swap.BaseReserve = %s, want %s", swap.BaseReserve, original.Swaps[0].BaseReserve)
}
if !swap.QuoteReserve.Equal(original.Swaps[0].QuoteReserve) {
t.Fatalf("swap.QuoteReserve = %s, want %s", swap.QuoteReserve, original.Swaps[0].QuoteReserve)
}
if !swap.UserBaseBalance.Equal(original.Swaps[0].UserBaseBalance) {
t.Fatalf("swap.UserBaseBalance = %s, want %s", swap.UserBaseBalance, original.Swaps[0].UserBaseBalance)
}
if !swap.UserQuoteBalance.Equal(original.Swaps[0].UserQuoteBalance) {
t.Fatalf("swap.UserQuoteBalance = %s, want %s", swap.UserQuoteBalance, original.Swaps[0].UserQuoteBalance)
}
if swap.AfterSOLBalance.StringFixed(9) != original.Swaps[0].AfterSOLBalance.StringFixed(9) {
t.Fatalf("swap.AfterSOLBalance = %s, want %s", swap.AfterSOLBalance, original.Swaps[0].AfterSOLBalance)
}
if swap.ActiveBinId != 0 {
t.Fatalf("swap.ActiveBinId = %d, want 0", swap.ActiveBinId)
}
if !swap.FeeAmount.IsZero() {
t.Fatalf("swap.FeeAmount = %s, want 0", swap.FeeAmount)
}
if swap.FeeBps != "" {
t.Fatalf("swap.FeeBps = %q, want empty", swap.FeeBps)
}
if swap.FeeSide != "" {
t.Fatalf("swap.FeeSide = %q, want empty", swap.FeeSide)
}
if swap.ConsumeUnit != 0 {
t.Fatalf("swap.ConsumeUnit = %d, want 0", swap.ConsumeUnit)
}
}
func TestTxBinaryRejectsUnknownProgramEnum(t *testing.T) {
txBinary := &TxBinary{
SchemaVersion: txBinarySchemaVersionCurrent,
EnumVersion: txBinaryEnumVersionV1,
Swaps: []SwapBinary{
{Program: "unknown_program"},
},
}
if _, err := txBinary.MarshalBinary(); err == nil {
t.Fatal("MarshalBinary() error = nil, want error")
}
}
2026-04-20 14:16:20 +08:00
func TestTxBinaryAcceptsKnownEventEnums(t *testing.T) {
events := []string{
TxEventAddLP,
TxEventRemoveLP,
TxEventBuy,
TxEventSell,
TxEventBuyFailed,
TxEventSellFailed,
TxEventBurn,
TxEventCreate,
TxEventComplete,
TxEventMigrate,
TxEventDeposit,
TxEventWithdraw,
TxEventOpen,
TxEventClose,
TxEventClaimFee,
TxEventAddLiquidity,
TxEventAddLiquidityOneSide,
TxEventRemoveLiquidity,
TxEventRemoveLiquidityOneSide,
}
for _, event := range events {
t.Run(event, func(t *testing.T) {
txBinary := &TxBinary{
SchemaVersion: txBinarySchemaVersionCurrent,
EnumVersion: txBinaryEnumVersionV1,
AddressTable: []solana.PublicKey{
mustPubKey("11111111111111111111111111111111"),
mustPubKey("So11111111111111111111111111111111111111112"),
solana.TokenProgramID,
mustPubKey("BPFLoader1111111111111111111111111111111111"),
mustPubKey("SysvarRent111111111111111111111111111111111"),
},
Swaps: []SwapBinary{
{
Program: SolProgramPump,
Event: event,
Pool: 0,
BaseMint: 1,
QuoteMint: 1,
BaseTokenProgram: 2,
QuoteTokenProgram: 2,
Creator: 3,
User: 4,
FixedMint: 1,
LimitMint: 1,
},
},
}
encoded, err := txBinary.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary() error = %v", err)
}
var decoded TxBinary
if err := decoded.UnmarshalBinary(encoded); err != nil {
t.Fatalf("UnmarshalBinary() error = %v", err)
}
if got := decoded.Swaps[0].Event; got != event {
t.Fatalf("decoded event = %q, want %q", got, event)
}
})
}
}
2026-04-20 15:09:42 +08:00
func TestTxBinaryPreservesFractionalReserves(t *testing.T) {
tx := &Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 1,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
ComputeUnitsConsumed: 1,
CuLimit: 1,
Swaps: []Swap{
{
Program: SolProgramMeteoraPools,
Event: TxEventAddLiquidity,
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
BaseAmount: decimal.NewFromInt(10),
QuoteAmount: decimal.NewFromInt(20),
SwapMode: SwapModeExactIn,
FixedAmount: decimal.NewFromInt(20),
FixedAmountSide: SwapAmountSideQuote,
FixedMint: solana.WrappedSol,
LimitAmountType: SwapLimitTypeMinOut,
LimitAmount: decimal.NewFromInt(9),
LimitAmountSide: SwapAmountSideBase,
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
ActualLimitAmount: decimal.NewFromInt(10),
ActualLimitAmountSide: SwapAmountSideBase,
BaseReserve: decimal.RequireFromString("123.4"),
QuoteReserve: decimal.RequireFromString("710079483.625409498"),
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
},
},
}
encoded, err := EncodeTxBinary(tx)
if err != nil {
t.Fatalf("EncodeTxBinary() error = %v", err)
}
decoded, err := DecodeTxBinary(encoded)
if err != nil {
t.Fatalf("DecodeTxBinary() error = %v", err)
}
if got := decoded.Swaps[0].BaseReserve.String(); got != "123.4" {
t.Fatalf("BaseReserve = %s, want 123.4", got)
}
diff := decoded.Swaps[0].QuoteReserve.Sub(decimal.RequireFromString("710079483.625409498")).Abs()
if diff.GreaterThan(decimal.RequireFromString("0.0000001")) {
t.Fatalf("QuoteReserve diff = %s, want <= 0.0000001", diff)
}
}
2026-04-20 15:25:08 +08:00
func TestTxBinaryCanonicalizesOnSideEventAlias(t *testing.T) {
tx := &Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 1,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
ComputeUnitsConsumed: 1,
CuLimit: 1,
Swaps: []Swap{
{
Program: SolProgramOrcaWhirPool,
Event: "remove_liquidity_on_side",
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
BaseAmount: decimal.NewFromInt(10),
QuoteAmount: decimal.Zero,
SwapMode: SwapModeExactIn,
FixedAmount: decimal.NewFromInt(10),
FixedAmountSide: SwapAmountSideBase,
FixedMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
LimitAmountType: SwapLimitTypeMinOut,
LimitAmount: decimal.Zero,
LimitAmountSide: SwapAmountSideQuote,
ActualLimitAmount: decimal.Zero,
ActualLimitAmountSide: SwapAmountSideQuote,
BaseReserve: decimal.RequireFromString("123.4"),
QuoteReserve: decimal.RequireFromString("456.7"),
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
},
},
}
encoded, err := EncodeTxBinary(tx)
if err != nil {
t.Fatalf("EncodeTxBinary() error = %v", err)
}
decoded, err := DecodeTxBinary(encoded)
if err != nil {
t.Fatalf("DecodeTxBinary() error = %v", err)
}
if got := decoded.Swaps[0].Event; got != TxEventRemoveLiquidityOneSide {
t.Fatalf("Event = %q, want %q", got, TxEventRemoveLiquidityOneSide)
}
}
2026-04-16 16:40:21 +08:00
func TestTxsBinaryRoundTripWithSharedAddressTable(t *testing.T) {
tx1 := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 1,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1000),
CUPrice: decimal.RequireFromString("0.123456"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
ComputeUnitsConsumed: 100,
CuLimit: 200000,
Swaps: []Swap{
{
Program: SolProgramPump,
Event: TxEventBuy,
TxIndex: 1,
InstrIdx: 0,
InnerIdx: 0,
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
BaseAmount: decimal.NewFromInt(10),
QuoteAmount: decimal.NewFromInt(20),
SwapMode: SwapModeExactIn,
FixedAmount: decimal.NewFromInt(20),
FixedAmountSide: SwapAmountSideQuote,
FixedMint: solana.WrappedSol,
LimitAmountType: SwapLimitTypeMinOut,
LimitAmount: decimal.NewFromInt(9),
LimitAmountSide: SwapAmountSideBase,
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
ActualLimitAmount: decimal.NewFromInt(10),
ActualLimitAmountSide: SwapAmountSideBase,
SlippageBps: decimal.RequireFromString("100.2"),
BaseReserve: decimal.NewFromInt(100),
QuoteReserve: decimal.NewFromInt(200),
UserBaseBalance: decimal.NewFromInt(1),
UserQuoteBalance: decimal.NewFromInt(2),
EntryContract: solana.PublicKey{},
MigrateToPool: solana.PublicKey{},
MigrateTopProgram: solana.PublicKey{},
LpMint: solana.PublicKey{},
AfterSOLBalance: decimal.RequireFromString("0.800000000"),
},
},
}
tx2 := tx1
tx2.Block = 2
tx2.BlockIndex = 2
tx2.CuFee = decimal.NewFromInt(2000)
tx2.AfterSOLBalance = decimal.RequireFromString("0.700000000")
tx2.Swaps = []Swap{tx1.Swaps[0]}
tx2.Swaps[0].TxIndex = 2
tx2.Swaps[0].BaseAmount = decimal.NewFromInt(30)
tx2.Swaps[0].QuoteAmount = decimal.NewFromInt(40)
batchEncoded, err := EncodeTxsBinary([]Tx{tx1, tx2})
if err != nil {
t.Fatalf("EncodeTxsBinary() error = %v", err)
}
decoded, err := DecodeTxsBinary(batchEncoded)
if err != nil {
t.Fatalf("DecodeTxsBinary() error = %v", err)
}
if len(decoded) != 2 {
t.Fatalf("decoded len = %d, want 2", len(decoded))
}
if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer {
t.Fatalf("decoded signer mismatch")
}
if decoded[0].Swaps[0].Pool != tx1.Swaps[0].Pool || decoded[1].Swaps[0].Pool != tx2.Swaps[0].Pool {
t.Fatalf("decoded shared address mismatch")
}
single1, err := EncodeTxBinary(&tx1)
if err != nil {
t.Fatalf("EncodeTxBinary(tx1) error = %v", err)
}
single2, err := EncodeTxBinary(&tx2)
if err != nil {
t.Fatalf("EncodeTxBinary(tx2) error = %v", err)
}
if len(batchEncoded) >= len(single1)+len(single2) {
t.Fatalf("batch encoded = %d, want smaller than singles sum %d", len(batchEncoded), len(single1)+len(single2))
}
}
func TestDecodeTxsBinaryReader(t *testing.T) {
tx1 := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 100,
BlockIndex: 7,
CuFee: decimal.NewFromInt(111),
CUPrice: decimal.RequireFromString("0.123456"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.500000000"),
ComputeUnitsConsumed: 1234,
CuLimit: 250000,
Swaps: []Swap{
{
Program: SolProgramPump,
Event: TxEventBuy,
TxIndex: 3,
InstrIdx: 1,
InnerIdx: 2,
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
BaseMintDecimals: 6,
QuoteMintDecimals: 9,
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
BaseAmount: decimal.NewFromInt(100),
QuoteAmount: decimal.NewFromInt(200),
SwapMode: SwapModeExactIn,
FixedAmount: decimal.NewFromInt(200),
FixedAmountSide: SwapAmountSideQuote,
FixedMint: solana.WrappedSol,
LimitAmountType: SwapLimitTypeMinOut,
LimitAmount: decimal.NewFromInt(90),
LimitAmountSide: SwapAmountSideBase,
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
ActualLimitAmount: decimal.NewFromInt(100),
ActualLimitAmountSide: SwapAmountSideBase,
SlippageBps: decimal.RequireFromString("99.6"),
BaseReserve: decimal.NewFromInt(1000),
QuoteReserve: decimal.NewFromInt(2000),
UserBaseBalance: decimal.NewFromInt(10),
UserQuoteBalance: decimal.NewFromInt(20),
AfterSOLBalance: decimal.RequireFromString("0.400000000"),
},
},
}
tx2 := tx1
tx2.Block = 101
tx2.BlockIndex = 8
tx2.CuFee = decimal.NewFromInt(222)
tx2.AfterSOLBalance = decimal.RequireFromString("0.300000000")
tx2.Swaps = []Swap{tx1.Swaps[0]}
tx2.Swaps[0].TxIndex = 4
tx2.Swaps[0].BaseAmount = decimal.NewFromInt(300)
encoded, err := EncodeTxsBinary([]Tx{tx1, tx2})
if err != nil {
t.Fatalf("EncodeTxsBinary() error = %v", err)
}
var decoded []*Tx
for tx, err := range DecodeTxsBinaryReader(bytes.NewReader(encoded)) {
if err != nil {
t.Fatalf("DecodeTxsBinaryReader() error = %v", err)
}
decoded = append(decoded, tx)
}
if len(decoded) != 2 {
t.Fatalf("decoded len = %d, want 2", len(decoded))
}
if decoded[0].Signer != tx1.Signer || decoded[1].Signer != tx2.Signer {
t.Fatalf("decoded signer mismatch")
}
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
t.Fatalf("decoded block mismatch")
}
if decoded[0].Swaps[0].BaseAmount.Cmp(tx1.Swaps[0].BaseAmount) != 0 {
t.Fatalf("decoded tx1 swap base amount = %s, want %s", decoded[0].Swaps[0].BaseAmount, tx1.Swaps[0].BaseAmount)
}
if decoded[1].Swaps[0].BaseAmount.Cmp(tx2.Swaps[0].BaseAmount) != 0 {
t.Fatalf("decoded tx2 swap base amount = %s, want %s", decoded[1].Swaps[0].BaseAmount, tx2.Swaps[0].BaseAmount)
}
}
func TestDecodeTxsBinaryReaderEarlyStop(t *testing.T) {
tx := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 1,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.999999999"),
ComputeUnitsConsumed: 1,
CuLimit: 1,
}
encoded, err := EncodeTxsBinary([]Tx{tx, tx, tx})
if err != nil {
t.Fatalf("EncodeTxsBinary() error = %v", err)
}
count := 0
for decodedTx, err := range DecodeTxsBinaryReader(bytes.NewReader(encoded)) {
if err != nil {
t.Fatalf("DecodeTxsBinaryReader() error = %v", err)
}
if decodedTx == nil {
t.Fatal("decoded tx is nil")
}
count++
break
}
if count != 1 {
t.Fatalf("count = %d, want 1", count)
}
}
func TestMergeTxsBinaryBytes(t *testing.T) {
tx1 := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 11,
BlockIndex: 1,
CuFee: decimal.NewFromInt(10),
CUPrice: decimal.RequireFromString("0.000123"),
BeforeSolBalance: decimal.RequireFromString("1.100000000"),
AfterSOLBalance: decimal.RequireFromString("1.000000000"),
ComputeUnitsConsumed: 10,
CuLimit: 100,
Swaps: []Swap{
{
Program: SolProgramPump,
Event: TxEventBuy,
TxIndex: 1,
Pool: mustPubKey("11111111111111111111111111111111"),
BaseMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("BPFLoader1111111111111111111111111111111111"),
User: mustPubKey("SysvarRent111111111111111111111111111111111"),
FixedMint: solana.WrappedSol,
LimitMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
EntryContract: solana.PublicKey{},
MigrateToPool: solana.PublicKey{},
MigrateTopProgram: solana.PublicKey{},
LpMint: solana.PublicKey{},
},
},
}
tx2 := Tx{
Signer: mustPubKey("SysvarRent111111111111111111111111111111111"),
Block: 12,
BlockIndex: 2,
CuFee: decimal.NewFromInt(20),
CUPrice: decimal.RequireFromString("0.000456"),
BeforeSolBalance: decimal.RequireFromString("2.200000000"),
AfterSOLBalance: decimal.RequireFromString("2.000000000"),
ComputeUnitsConsumed: 20,
CuLimit: 200,
Swaps: []Swap{
{
Program: SolProgramPump,
Event: TxEventSell,
TxIndex: 2,
Pool: mustPubKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
BaseMint: mustPubKey("4Nd1mJf8JQhRVTfJxW2YxXLNQKhPYo1JzN1u2KAPY1Hn"),
QuoteMint: solana.WrappedSol,
BaseTokenProgram: solana.TokenProgramID,
QuoteTokenProgram: solana.TokenProgramID,
Creator: mustPubKey("ComputeBudget111111111111111111111111111111"),
User: mustPubKey("So11111111111111111111111111111111111111112"),
FixedMint: solana.WrappedSol,
LimitMint: mustPubKey("4Nd1mJf8JQhRVTfJxW2YxXLNQKhPYo1JzN1u2KAPY1Hn"),
EntryContract: solana.PublicKey{},
MigrateToPool: solana.PublicKey{},
MigrateTopProgram: solana.PublicKey{},
LpMint: solana.PublicKey{},
},
},
}
batch1, err := EncodeTxsBinary([]Tx{tx1})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch1) error = %v", err)
}
batch2, err := EncodeTxsBinary([]Tx{tx2})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch2) error = %v", err)
}
merged, err := MergeTxsBinaryBytes([][]byte{batch1, batch2})
if err != nil {
t.Fatalf("MergeTxsBinaryBytes() error = %v", err)
}
var mergedBinary TxsBinary
if err := mergedBinary.UnmarshalBinary(merged); err != nil {
t.Fatalf("UnmarshalBinary(merged) error = %v", err)
}
if len(mergedBinary.Txs) != 2 {
t.Fatalf("merged tx count = %d, want 2", len(mergedBinary.Txs))
}
if len(mergedBinary.AddressTable) >= len(mustTxBinary(t, batch1).AddressTable)+len(mustTxBinary(t, batch2).AddressTable) {
t.Fatalf("merged address table was not deduplicated")
}
decoded, err := DecodeTxsBinary(merged)
if err != nil {
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
}
if len(decoded) != 2 {
t.Fatalf("decoded len = %d, want 2", len(decoded))
}
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block {
t.Fatalf("decoded block mismatch")
}
}
func TestMergeTxsBinarySourcesToWriterWithConcatenatedBatches(t *testing.T) {
tx1 := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 21,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
ComputeUnitsConsumed: 11,
CuLimit: 111,
}
tx2 := tx1
tx2.Block = 22
tx2.BlockIndex = 2
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
tx3 := tx1
tx3.Block = 23
tx3.BlockIndex = 3
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
batch1, err := EncodeTxsBinary([]Tx{tx1})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch1) error = %v", err)
}
batch2, err := EncodeTxsBinary([]Tx{tx2})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch2) error = %v", err)
}
batch3, err := EncodeTxsBinary([]Tx{tx3})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch3) error = %v", err)
}
source1 := &testTxsBinarySource{
data: append(append([]byte{}, batch1...), batch2...),
}
source2 := &testTxsBinarySource{
data: batch3,
}
var out bytes.Buffer
if err := MergeTxsBinarySourcesToWriter([]TxsBinaryReaderSource{source1, source2}, &out); err != nil {
t.Fatalf("MergeTxsBinarySourcesToWriter() error = %v", err)
}
if source1.opens != 2 || source2.opens != 2 {
t.Fatalf("source opens = (%d, %d), want (2, 2)", source1.opens, source2.opens)
}
decoded, err := DecodeTxsBinary(out.Bytes())
if err != nil {
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
}
if len(decoded) != 3 {
t.Fatalf("decoded len = %d, want 3", len(decoded))
}
if decoded[0].Block != tx1.Block || decoded[1].Block != tx2.Block || decoded[2].Block != tx3.Block {
t.Fatalf("decoded block order mismatch")
}
}
2026-04-16 17:56:17 +08:00
func TestMergeTxsBinarySourcesToWriterWithBatchHeaderFuncSkip(t *testing.T) {
tx1 := Tx{
Signer: mustPubKey("So11111111111111111111111111111111111111112"),
Block: 31,
BlockIndex: 1,
CuFee: decimal.NewFromInt(1),
CUPrice: decimal.RequireFromString("0.000001"),
BeforeSolBalance: decimal.RequireFromString("1.000000000"),
AfterSOLBalance: decimal.RequireFromString("0.900000000"),
ComputeUnitsConsumed: 11,
CuLimit: 111,
}
tx2 := tx1
tx2.Block = 32
tx2.BlockIndex = 2
tx2.Signer = mustPubKey("SysvarRent111111111111111111111111111111111")
tx3 := tx1
tx3.Block = 33
tx3.BlockIndex = 3
tx3.Signer = mustPubKey("ComputeBudget111111111111111111111111111111")
batch1, err := EncodeTxsBinary([]Tx{tx1})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch1) error = %v", err)
}
batch2, err := EncodeTxsBinary([]Tx{tx2})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch2) error = %v", err)
}
batch3, err := EncodeTxsBinary([]Tx{tx3})
if err != nil {
t.Fatalf("EncodeTxsBinary(batch3) error = %v", err)
}
source := &testTxsBinarySource{
data: append(
append(
append([]byte{}, testBatchHeader(false)...),
batch1...,
),
append(
append(testBatchHeader(true), batch2...),
append(testBatchHeader(false), batch3...)...,
)...,
),
}
var out bytes.Buffer
err = MergeTxsBinarySourcesToWriterWithOptions(
[]TxsBinaryReaderSource{source},
&out,
TxsBinaryMergeOptions{
BatchHeaderFunc: func(ctx *TxsBinaryBatchHeaderContext) (bool, error) {
header := make([]byte, 5)
if _, err := io.ReadFull(ctx.Reader, header); err != nil {
return false, err
}
if !bytes.Equal(header[:4], []byte("BHDR")) {
return false, io.ErrUnexpectedEOF
}
return header[4] == 1, nil
},
},
)
if err != nil {
t.Fatalf("MergeTxsBinarySourcesToWriterWithOptions() error = %v", err)
}
decoded, err := DecodeTxsBinary(out.Bytes())
if err != nil {
t.Fatalf("DecodeTxsBinary(merged) error = %v", err)
}
if len(decoded) != 2 {
t.Fatalf("decoded len = %d, want 2", len(decoded))
}
if decoded[0].Block != tx1.Block || decoded[1].Block != tx3.Block {
t.Fatalf("decoded block order mismatch after skip")
}
if source.opens != 2 {
t.Fatalf("source.opens = %d, want 2", source.opens)
}
}
2026-04-16 16:40:21 +08:00
func mustPubKey(value string) solana.PublicKey {
return solana.MustPublicKeyFromBase58(value)
}
func mustTxBinary(t *testing.T, data []byte) *TxsBinary {
t.Helper()
var txsBinary TxsBinary
if err := txsBinary.UnmarshalBinary(data); err != nil {
t.Fatalf("UnmarshalBinary() error = %v", err)
}
return &txsBinary
}
type testTxsBinarySource struct {
data []byte
opens int
}
func (s *testTxsBinarySource) OpenTxsBinaryReader() (io.ReadCloser, error) {
s.opens++
return io.NopCloser(bytes.NewReader(s.data)), nil
}
2026-04-16 17:56:17 +08:00
func testBatchHeader(skip bool) []byte {
header := []byte("BHDR\x00")
if skip {
header[4] = 1
}
return header
}