Files
pump-parser/swap_amounts_oracle_test.go
2026-04-16 14:24:14 +08:00

388 lines
15 KiB
Go

package pump_parser
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
"github.com/shopspring/decimal"
)
type swapOracleCase struct {
name string
txHash string
index int
program string
event string
swapMode SwapMode
fixedAmount string
fixedAmountSide SwapAmountSide
fixedMint string
limitAmountType SwapLimitType
limitAmount string
limitAmountSide SwapAmountSide
limitMint string
actualLimitAmount string
actualLimitAmountSide SwapAmountSide
slippageBps string
}
func TestSwapAmountOracleSamples(t *testing.T) {
EnableAllParsers()
cases := []swapOracleCase{
{
name: "pump buy exact out",
txHash: "5ybEYcXYhFNfCNAu1o7ovM1Rw5285PBzAwsj4ezwmPRLkYtXX91GhcvAgTVZvdCVV6upsGH8DwYeseNswPhEfVbg",
index: 0,
program: "Pump",
event: TxEventBuy,
swapMode: SwapModeExactOut,
fixedAmount: "1459556161603",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "CEwaxx5j1K61JMYXavcxihVQW4NxC6c4NQ27veFpYUYA",
limitAmountType: SwapLimitTypeMaxIn,
limitAmount: "100000001",
limitAmountSide: SwapAmountSideQuote,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "98765431",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "123.4569987654300123",
},
{
name: "raydium v4 exact out",
txHash: "3Q1BhUvqm889oJfCn96AZAJqyHi1uxdeoKQknCc9xQcajhZS5APfwzkHuNTkPJEhyGknj7VyLpkNoMmNPK3No6hC",
index: 0,
program: "RaydiumV4",
event: TxEventSell,
swapMode: SwapModeExactOut,
fixedAmount: "432588",
fixedAmountSide: SwapAmountSideQuote,
fixedMint: "2qEHjDLDLbuBgRYvsxhc5D6uDWAivNFZGan56P1tpump",
limitAmountType: SwapLimitTypeMaxIn,
limitAmount: "18446744073709551615",
limitAmountSide: SwapAmountSideBase,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "279099",
actualLimitAmountSide: SwapAmountSideBase,
slippageBps: "9999.9999999998487001",
},
{
name: "pump amm exact in",
txHash: "3Q1BhUvqm889oJfCn96AZAJqyHi1uxdeoKQknCc9xQcajhZS5APfwzkHuNTkPJEhyGknj7VyLpkNoMmNPK3No6hC",
index: 1,
program: "PumpAMM",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "432588",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "2qEHjDLDLbuBgRYvsxhc5D6uDWAivNFZGan56P1tpump",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideQuote,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "284317",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "10000",
},
{
name: "meteora dlmm exact in",
txHash: "5uw9Uwe9KDLCzUNf1sRr7EjKqE2Xs8qUHaoC1CRFhkE3TMpo5TimwApW65pd6pmB7Cp92XXMaZ9jQav6aXRZGtoS",
index: 0,
program: "MeteoraDLMM",
event: TxEventBuy,
swapMode: SwapModeExactIn,
fixedAmount: "17684137",
fixedAmountSide: SwapAmountSideQuote,
fixedMint: "So11111111111111111111111111111111111111112",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideBase,
limitMint: "METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m",
actualLimitAmount: "50437818",
actualLimitAmountSide: SwapAmountSideBase,
slippageBps: "10000",
},
{
name: "orca whirlpool exact in",
txHash: "5uw9Uwe9KDLCzUNf1sRr7EjKqE2Xs8qUHaoC1CRFhkE3TMpo5TimwApW65pd6pmB7Cp92XXMaZ9jQav6aXRZGtoS",
index: 1,
program: "OrcaWhirPool",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "50437818",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideQuote,
limitMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
actualLimitAmount: "1438802",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "10000",
},
{
name: "raydium v4 exact in",
txHash: "5uw9Uwe9KDLCzUNf1sRr7EjKqE2Xs8qUHaoC1CRFhkE3TMpo5TimwApW65pd6pmB7Cp92XXMaZ9jQav6aXRZGtoS",
index: 2,
program: "RaydiumV4",
event: TxEventBuy,
swapMode: SwapModeExactIn,
fixedAmount: "1438802",
fixedAmountSide: SwapAmountSideQuote,
fixedMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideBase,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "19059759",
actualLimitAmountSide: SwapAmountSideBase,
slippageBps: "10000",
},
{
name: "raydium clmm exact in",
txHash: "3XoRKna49qCAuF75ctmaYupNmYWuFm5AU73ULQjxNUxz9qJzuKqMRqq5Z88L6DooWTF44UxnxMXwqLn5t9NsoCoZ",
index: 2,
program: "RaydiumCLMM",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "1569519567845",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "CiKu4eHsVrc1eueVQeHn7qhXTcVu95gSQmBpX4utjL9z",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideQuote,
limitMint: "6sQgvhAYtYFrahcjB1hKfB3ZC5YDVdfYvAqK1GKe93c9",
actualLimitAmount: "366578",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "10000",
},
{
name: "raydium cpmm exact in",
txHash: "288FAsrj7h6hTKywtVaqCqHAbNZ6x3Xuich9kQMGVarVUnUjkqTabxQE9JHyranGY9eqUivZbBTzC5dH1BEuJ6pa",
index: 2,
program: "RaydiumCPMM",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "1260040377905",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "3f7wfg9yHLtGKvy75MmqsVT1ueTFoqyySQbusrX1YAQ4",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "0",
limitAmountSide: SwapAmountSideQuote,
limitMint: "oreoU2P8bN6jkk3jbaiVxYnG1dCXcYxwhwyK9jSybcp",
actualLimitAmount: "802507591",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "10000",
},
{
name: "raydium launchlab exact in",
txHash: "1r3gfEse3WAy5H6h4jMSNq1K5KZNCrMdAtCnpBSE1xkHQEt3EJ2J6Lk6ihQshrfsrS5FbqP5WuUSZG6zPCJB5TE",
index: 0,
program: "RaydiumLaunchLab",
event: TxEventBuy,
swapMode: SwapModeExactIn,
fixedAmount: "10000000",
fixedAmountSide: SwapAmountSideQuote,
fixedMint: "So11111111111111111111111111111111111111112",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "5976144139694",
limitAmountSide: SwapAmountSideBase,
limitMint: "Attr2sqaXr76XqaDxdtnQ4QAEsaFdgTGr599F7ytgray",
actualLimitAmount: "6129378604814",
actualLimitAmountSide: SwapAmountSideBase,
slippageBps: "249.9999999994289796",
},
{
name: "meteora pools exact in",
txHash: "5jQk6mbhtExpUFskRy2AfKWbLgXDv2USiGkq9tQWauGVKduGdTqscgxyDCPgBryr4kz5hDT5CE9TpVTKDoPhkBmt",
index: 0,
program: "MeteoraPools",
event: TxEventBuy,
swapMode: SwapModeExactIn,
fixedAmount: "75404052467",
fixedAmountSide: SwapAmountSideQuote,
fixedMint: "STrikemJEk2tFVYpg7SMo9nGPrnJ56fHnS1K7PV2fPw",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "30605141",
limitAmountSide: SwapAmountSideBase,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "31556751",
actualLimitAmountSide: SwapAmountSideBase,
slippageBps: "301.5551252408715967",
},
{
name: "meteora bonding curve exact in",
txHash: "5Qsq1ueenSs4KgVRgwXmBVFvMR3Asq9MmXwmqQimxDdWLdiJy6dVfmYqa2YCvkNH1Gx7aCzJqg4t9gN9ECfxH2JS",
index: 0,
program: "MeteoraBondingCurve",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "11022737683",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "8FosqFryatEMV4ZeFR1gLmSmxBLcQ2NCibpZxFRPPF34",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "49672101",
limitAmountSide: SwapAmountSideQuote,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "49672101",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "0",
},
{
name: "meteora damm v2 exact in",
txHash: "43EouSYkeVmLBZSKW1KptiQcAVvB6KX49wjztjgzkQ9iU38A6fF68k77bNy9Wn6fwjykqYsPorUKj8m6SFY7naf1",
index: 0,
program: "MeteoraAmmV2",
event: TxEventSell,
swapMode: SwapModeExactIn,
fixedAmount: "11846",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "So11111111111111111111111111111111111111112",
limitAmountType: SwapLimitTypeMinOut,
limitAmount: "30893426",
limitAmountSide: SwapAmountSideQuote,
limitMint: "CdDoeyd67nuzmMCF8Dd3RzbxiTRk41Xd922Veu9kGvDE",
actualLimitAmount: "33325162",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "729.6996785792069068",
},
{
name: "meteora damm v2 exact out",
txHash: "BD7GZaXaJc2hzSNPe6Q5yeej7rZLQFMpdx4rZwPhGTyHP43iMAR7LxymRSPGXnefAxSqi5sMsEPS1cjyQjup3Eu",
index: 0,
program: "MeteoraAmmV2",
event: TxEventBuy,
swapMode: SwapModeExactOut,
fixedAmount: "512761043",
fixedAmountSide: SwapAmountSideBase,
fixedMint: "DPfZc59DLrKyVTJDoKB8CBFgCndsjUzxy6fdbxk4Zms9",
limitAmountType: SwapLimitTypeMaxIn,
limitAmount: "71386496",
limitAmountSide: SwapAmountSideQuote,
limitMint: "So11111111111111111111111111111111111111112",
actualLimitAmount: "70020377",
actualLimitAmountSide: SwapAmountSideQuote,
slippageBps: "191.3693872857970225",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
tx := mustParseRPCFixtureTx(t, tc.txHash)
if tc.index >= len(tx.Swaps) {
t.Fatalf("swap index %d out of range, len=%d", tc.index, len(tx.Swaps))
}
swap := tx.Swaps[tc.index]
if swap.Program != tc.program {
t.Fatalf("program = %q, want %q", swap.Program, tc.program)
}
if swap.Event != tc.event {
t.Fatalf("event = %q, want %q", swap.Event, tc.event)
}
if swap.SwapMode != tc.swapMode {
t.Fatalf("swap mode = %s, want %s", swap.SwapMode.String(), tc.swapMode.String())
}
assertDecimalString(t, "fixed_amount", swap.FixedAmount, tc.fixedAmount)
if swap.FixedAmountSide != tc.fixedAmountSide {
t.Fatalf("fixed amount side = %s, want %s", swap.FixedAmountSide.String(), tc.fixedAmountSide.String())
}
assertPublicKey(t, "fixed_mint", swap.FixedMint, tc.fixedMint)
if swap.LimitAmountType != tc.limitAmountType {
t.Fatalf("limit amount type = %s, want %s", swap.LimitAmountType.String(), tc.limitAmountType.String())
}
assertDecimalString(t, "limit_amount", swap.LimitAmount, tc.limitAmount)
if swap.LimitAmountSide != tc.limitAmountSide {
t.Fatalf("limit amount side = %s, want %s", swap.LimitAmountSide.String(), tc.limitAmountSide.String())
}
assertPublicKey(t, "limit_mint", swap.LimitMint, tc.limitMint)
assertDecimalString(t, "actual_limit_amount", swap.ActualLimitAmount, tc.actualLimitAmount)
if swap.ActualLimitAmountSide != tc.actualLimitAmountSide {
t.Fatalf("actual limit amount side = %s, want %s", swap.ActualLimitAmountSide.String(), tc.actualLimitAmountSide.String())
}
assertDecimalString(t, "slippage_bps", swap.SlippageBps, tc.slippageBps)
})
}
}
func mustParseRPCFixtureTx(t *testing.T, txHash string) *Tx {
t.Helper()
fixturePath := filepath.Join("testdata", "rpc", txHash+".json")
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
}
func assertDecimalString(t *testing.T, field string, got decimal.Decimal, want string) {
t.Helper()
wantDecimal, err := decimal.NewFromString(want)
if err != nil {
t.Fatalf("invalid expected decimal for %s: %v", field, err)
}
if !got.Equal(wantDecimal) {
t.Fatalf("%s = %s, want %s", field, got.String(), want)
}
}
func assertPublicKey(t *testing.T, field string, got solana.PublicKey, want string) {
t.Helper()
wantKey := solana.MustPublicKeyFromBase58(want)
if !got.Equals(wantKey) {
t.Fatalf("%s = %s, want %s", field, got, wantKey)
}
}