batch encode

This commit is contained in:
thloyi
2026-04-16 16:40:21 +08:00
parent e761fd6f84
commit d2879efcc6
5 changed files with 3014 additions and 5 deletions

View File

@@ -0,0 +1,134 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
pump_parser "github.com/thloyi/pump-parser"
)
type blockResponse struct {
Result blockResult `json:"result"`
}
type blockResult struct {
BlockTime *int64 `json:"blockTime"`
Transactions []pump_parser.RawTx `json:"transactions"`
}
func main() {
var (
filePath = flag.String("file", "", "path to getBlock payload json")
slot = flag.Uint64("slot", 0, "block slot")
swapsOnly = flag.Bool("swaps-only", false, "only include transactions with swaps > 0")
)
flag.Parse()
if *filePath == "" || *slot == 0 {
fmt.Fprintln(os.Stderr, "usage: measure_tx_binary_block -file /path/block.json -slot 413539056")
os.Exit(2)
}
raw, err := os.ReadFile(*filePath)
if err != nil {
fmt.Fprintf(os.Stderr, "read file: %v\n", err)
os.Exit(1)
}
var response blockResponse
if err := json.Unmarshal(raw, &response); err != nil {
fmt.Fprintf(os.Stderr, "unmarshal block payload: %v\n", err)
os.Exit(1)
}
var blockTime *uint64
if response.Result.BlockTime != nil {
bt := uint64(*response.Result.BlockTime)
blockTime = &bt
}
total := len(response.Result.Transactions)
converted := 0
parsed := 0
convertFailures := 0
parseFailures := 0
encodeFailures := 0
filteredOutNoSwaps := 0
var totalRawTxBytes int
var totalSingleEncoded int
minSingleEncoded := -1
maxSingleEncoded := 0
parsedTxs := make([]pump_parser.Tx, 0, total)
for i, rawTx := range response.Result.Transactions {
transactionJSON, err := json.Marshal(rawTx.Transaction)
if err == nil {
totalRawTxBytes += len(transactionJSON)
}
rawTx.BlockTime = 0
if blockTime != nil {
rawTx.BlockTime = int64(*blockTime)
}
rawTx.Slot = *slot
rawTx.IndexWithinBlock = int64(i)
converted++
tx, err := pump_parser.ParseRawTx(&rawTx)
if err != nil {
parseFailures++
continue
}
if *swapsOnly && len(tx.Swaps) == 0 {
filteredOutNoSwaps++
continue
}
parsed++
encoded, err := pump_parser.EncodeTxBinary(tx)
if err != nil {
encodeFailures++
continue
}
size := len(encoded)
totalSingleEncoded += size
if minSingleEncoded == -1 || size < minSingleEncoded {
minSingleEncoded = size
}
if size > maxSingleEncoded {
maxSingleEncoded = size
}
parsedTxs = append(parsedTxs, *tx)
}
batchEncoded, err := pump_parser.EncodeTxsBinary(parsedTxs)
if err != nil {
fmt.Fprintf(os.Stderr, "encode txs binary: %v\n", err)
os.Exit(1)
}
avgSingleEncoded := 0
if parsed > 0 {
avgSingleEncoded = totalSingleEncoded / parsed
}
fmt.Printf("block_slot=%d\n", *slot)
fmt.Printf("payload_json_bytes=%d\n", len(raw))
fmt.Printf("transactions_total=%d\n", total)
fmt.Printf("transactions_converted=%d\n", converted)
fmt.Printf("transactions_parsed=%d\n", parsed)
fmt.Printf("transactions_filtered_no_swaps=%d\n", filteredOutNoSwaps)
fmt.Printf("convert_failures=%d\n", convertFailures)
fmt.Printf("parse_failures=%d\n", parseFailures)
fmt.Printf("encode_failures=%d\n", encodeFailures)
fmt.Printf("raw_tx_total_bytes=%d\n", totalRawTxBytes)
fmt.Printf("single_txbinary_total_bytes=%d\n", totalSingleEncoded)
fmt.Printf("single_txbinary_avg_bytes=%d\n", avgSingleEncoded)
fmt.Printf("single_txbinary_min_bytes=%d\n", minSingleEncoded)
fmt.Printf("single_txbinary_max_bytes=%d\n", maxSingleEncoded)
fmt.Printf("batch_shared_table_bytes=%d\n", len(batchEncoded))
if totalSingleEncoded > 0 {
fmt.Printf("batch_vs_single_saved_bytes=%d\n", totalSingleEncoded-len(batchEncoded))
}
}

12
enum.go
View File

@@ -119,9 +119,11 @@ func GetConditionByProgram(program string) []string {
} }
const ( const (
TxEventAddLP = "add" TxEventAddLP = "add"
TxEventRemoveLP = "remove" TxEventRemoveLP = "remove"
TxEventBuy = "buy" TxEventBuy = "buy"
TxEventSell = "sell" TxEventSell = "sell"
TxEventBurn = "burn" TxEventBuyFailed = "buy_failed"
TxEventSellFailed = "sell_failed"
TxEventBurn = "burn"
) )

2100
tx_binary.go Normal file

File diff suppressed because it is too large Load Diff

146
tx_binary_realdata_test.go Normal file
View File

@@ -0,0 +1,146 @@
package pump_parser
import (
"encoding/json"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"github.com/gagliardetto/solana-go/rpc"
)
func TestTxBinaryRealFixtureSizes(t *testing.T) {
fixtures, err := filepath.Glob(filepath.Join("testdata", "rpc", "*.json"))
if err != nil {
t.Fatalf("glob fixtures: %v", err)
}
if len(fixtures) == 0 {
t.Fatal("no rpc fixtures found")
}
sort.Strings(fixtures)
type sizeResult struct {
name string
swaps int
platforms int
mevAgents int
addresses int
encodedBytes int
fixtureBytes int
txBinaryBytes int
}
results := make([]sizeResult, 0, len(fixtures))
totalEncoded := 0
for _, fixture := range fixtures {
tx, rawTxBytesLen, fixtureBytesLen := mustParseRPCFixtureTxForBinarySize(t, fixture)
binaryTx, err := NewTxBinary(tx)
if err != nil {
t.Fatalf("build tx binary fixture %s: %v", fixture, err)
}
encoded, err := binaryTx.MarshalBinary()
if err != nil {
t.Fatalf("encode fixture %s: %v", fixture, err)
}
result := sizeResult{
name: strings.TrimSuffix(filepath.Base(fixture), filepath.Ext(fixture)),
swaps: len(tx.Swaps),
platforms: len(tx.Platform),
mevAgents: len(tx.MevAgent),
addresses: len(binaryTx.AddressTable),
encodedBytes: len(encoded),
fixtureBytes: fixtureBytesLen,
txBinaryBytes: rawTxBytesLen,
}
results = append(results, result)
totalEncoded += result.encodedBytes
}
for _, result := range results {
t.Logf(
"%s encoded=%dB swaps=%d platforms=%d mev=%d addresses=%d fixture_json=%dB raw_tx=%dB",
result.name,
result.encodedBytes,
result.swaps,
result.platforms,
result.mevAgents,
result.addresses,
result.fixtureBytes,
result.txBinaryBytes,
)
}
minResult := results[0]
maxResult := results[0]
for _, result := range results[1:] {
if result.encodedBytes < minResult.encodedBytes {
minResult = result
}
if result.encodedBytes > maxResult.encodedBytes {
maxResult = result
}
}
t.Logf(
"summary fixtures=%d avg=%dB min=%dB(%s) max=%dB(%s)",
len(results),
totalEncoded/len(results),
minResult.encodedBytes,
minResult.name,
maxResult.encodedBytes,
maxResult.name,
)
}
func mustParseRPCFixtureTxForBinarySize(t *testing.T, fixturePath string) (*Tx, int, int) {
t.Helper()
raw, err := os.ReadFile(fixturePath)
if err != nil {
t.Fatalf("read fixture %s: %v", fixturePath, err)
}
var response struct {
Result *rpc.GetTransactionResult `json:"result"`
}
if err := json.Unmarshal(raw, &response); err != nil {
t.Fatalf("unmarshal fixture %s: %v", fixturePath, err)
}
if response.Result == nil || response.Result.Transaction == nil || response.Result.Meta == nil {
t.Fatalf("fixture %s is missing transaction data", fixturePath)
}
rawBinary := response.Result.Transaction.GetBinary()
if len(rawBinary) == 0 {
t.Fatalf("fixture %s has empty transaction bytes", fixturePath)
}
txWithMeta := rpc.TransactionWithMeta{
Slot: response.Result.Slot,
BlockTime: response.Result.BlockTime,
Transaction: rpc.DataBytesOrJSONFromBytes(rawBinary),
Meta: response.Result.Meta,
Version: response.Result.Version,
}
var blockTime *uint64
if response.Result.BlockTime != nil {
bt := uint64(*response.Result.BlockTime)
blockTime = &bt
}
rawTx, err := FromRpcTransactionWithMeta(txWithMeta, blockTime, response.Result.Slot, 0)
if err != nil {
t.Fatalf("convert fixture %s: %v", fixturePath, err)
}
tx, err := ParseRawTx(rawTx)
if err != nil {
t.Fatalf("parse fixture %s: %v", fixturePath, err)
}
return tx, len(rawBinary), len(raw)
}

627
tx_binary_test.go Normal file
View File

@@ -0,0 +1,627 @@
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")
}
}
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")
}
}
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
}