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) } }