Compare commits

...

15 Commits

Author SHA1 Message Date
thloyi
51f1511c8f fix EncodeTxBinary 2026-04-20 15:09:42 +08:00
thloyi
7dfe003e5b fix event enum 2026-04-20 14:16:20 +08:00
thloyi
fe94888b14 fix slippage 2026-04-20 12:31:30 +08:00
thloyi
1dd843c393 batch encode opts 2026-04-16 17:56:17 +08:00
thloyi
d2879efcc6 batch encode 2026-04-16 16:40:40 +08:00
thloyi
e761fd6f84 swap amount input 2026-04-16 14:24:14 +08:00
thloyi
ab0e87a48a fix raydium v4 swap v2 2026-04-16 11:39:15 +08:00
bijianing97
fb8d93f426 Update dlmm fee 2026-04-11 08:34:21 +08:00
bijianing97
0cc843b370 Update dlmm fee 2026-04-11 08:27:34 +08:00
bijianing97
d9a214b4b4 Add dlmm add liquidity one side function 2026-03-25 11:34:46 +08:00
thloyi
047b549d0f option ComputeUnitsConsumed 2026-03-23 20:20:21 +08:00
bijianing97
9327eab010 Fix dlmm parser 2026-03-23 15:30:43 +08:00
bijianing97
0ef57cf79a Add dlmm add_liquidity_by_weight 2026-03-20 17:06:37 +08:00
cachalots
03030d817d update 2026-03-20 11:41:44 +08:00
bijianing97
401dca225a Add dlmm open and close 2026-03-19 14:10:14 +08:00
40 changed files with 6670 additions and 386 deletions

156
SLIPPAGE_MAPPING.md Normal file
View File

@@ -0,0 +1,156 @@
# Slippage Mapping
This document describes how `SlippageBps` is derived for each supported swap protocol in this repository.
## Unified Fields
Each parsed `Swap` may include these normalized fields:
- `SwapMode`
- `FixedAmount`
- `FixedAmountSide`
- `FixedMint`
- `LimitAmountType`
- `LimitAmount`
- `LimitAmountSide`
- `LimitMint`
- `ActualLimitAmount`
- `ActualLimitAmountSide`
- `SlippageBps`
## Internal Enum Mapping
These fields are stored internally as `uint8` enums and serialized as strings in JSON / debug output.
### `SwapMode`
| Raw Value | Name | Serialized Value |
| --- | --- | --- |
| `0` | `SwapModeUnknown` | `""` |
| `1` | `SwapModeExactIn` | `"exact_in"` |
| `2` | `SwapModeExactOut` | `"exact_out"` |
### `SwapAmountSide`
Used by:
- `FixedAmountSide`
- `LimitAmountSide`
- `ActualLimitAmountSide`
| Raw Value | Name | Serialized Value |
| --- | --- | --- |
| `0` | `SwapAmountSideUnknown` | `""` |
| `1` | `SwapAmountSideBase` | `"base"` |
| `2` | `SwapAmountSideQuote` | `"quote"` |
### `SwapLimitType`
Used by:
- `LimitAmountType`
| Raw Value | Name | Serialized Value |
| --- | --- | --- |
| `0` | `SwapLimitTypeUnknown` | `""` |
| `1` | `SwapLimitTypeMinOut` | `"min_out"` |
| `2` | `SwapLimitTypeMaxIn` | `"max_in"` |
## Calculation Rules
- `exact_in`
- `SlippageBps = (actual_out - min_out) / actual_out * 10000`
- `exact_out`
- `SlippageBps = (max_in - actual_in) / max_in * 10000`
Interpretation:
- Positive: execution is better than the user limit
- Zero: execution lands exactly on the user limit
- `10000`: user limit is effectively unbounded on the constrained side (for example `min_out = 0`)
- Negative raw headroom is clamped to `0` because successful-swap storage uses a non-negative bounded metric
This definition makes `SlippageBps` a bounded "remaining headroom to the user's limit" metric for successful swaps:
- `exact_in`: how much output headroom remained, measured against the realized output
- `exact_out`: how much input headroom remained, measured against the allowed max input
## Protocol Mapping
| Protocol | Method Semantics | `SwapMode` | `FixedAmount` | `LimitAmountType` | `LimitAmount` | `ActualLimitAmount` |
| --- | --- | --- | --- | --- | --- | --- |
| `Pump` | `buy` | `exact_out` | target token amount | `max_in` | max SOL in | actual SOL in |
| `Pump` | `buy_exact_sol_in` | `exact_in` | SOL in | `min_out` | min token out | actual token out |
| `Pump` | `sell` | `exact_in` | token in | `min_out` | min SOL out | actual SOL out |
| `PumpAMM` | `buy` | `exact_out` | target base out | `max_in` | max quote in | actual quote in |
| `PumpAMM` | `buy_exact_quote_in` | `exact_in` | quote in | `min_out` | min base out | actual base out |
| `PumpAMM` | `sell` | `exact_in` | base in | `min_out` | min quote out | actual quote out |
| `MeteoraDLMM` | `swap` / `swap2` / `swap_with_price_impact` | `exact_in` | `AmountIn` | `min_out` | instruction min out | event output |
| `MeteoraDLMM` | `swap_exact_out` / `swap_exact_out2` | `exact_out` | `OutAmount` | `max_in` | `MaxInAmount` | event input |
| `MeteoraPools` | `swap` | `exact_in` | `InAmount` | `min_out` | `MinimumOutAmount` | actual output side |
| `MeteoraBondingCurve` | `swap` / `swap2` | `exact_in` | `AmountIn` | `min_out` | `MinimumAmountOut` | actual output side |
| `MeteoraAmmV2` | `swap` / `swap2` exact-in or partial | `exact_in` | params input side | `min_out` | params output threshold | actual output side |
| `MeteoraAmmV2` | `swap` / `swap2` exact-out | `exact_out` | params target output | `max_in` | params max input | actual input side |
| `RaydiumLaunchLab` | `*_ExactIn` | `exact_in` | `Amount` | `min_out` | `OtherAmountThreshold` | actual output side |
| `RaydiumLaunchLab` | `*_ExactOut` | `exact_out` | `Amount` | `max_in` | `OtherAmountThreshold` | actual input side |
| `RaydiumCPMM` | `swap_base_input` | `exact_in` | `AmountIn` | `min_out` | `MinimumAmountOut` | actual output side |
| `RaydiumCPMM` | `swap_base_output` | `exact_out` | `AmountOut` | `max_in` | `MaxAmountIn` | actual input side |
| `RaydiumCLMM` | `swap` / `swap_v2` | `exact_in` or `exact_out` | `amount` | `min_out` or `max_in` | `other_amount_threshold` | opposite-side actual amount |
| `RaydiumV4` | `swap_base_in` / `swap_base_in_v2` | `exact_in` | `amount_in` | `min_out` | `minimum_amount_out` | actual output side |
| `RaydiumV4` | `swap_base_out` / `swap_base_out_v2` | `exact_out` | `amount_out` | `max_in` | `max_amount_in` | actual input side |
| `OrcaWhirlpool` | `swap` / `swap_v2` | `exact_in` or `exact_out` | `amount` | `min_out` or `max_in` | `other_amount_threshold` | opposite-side actual amount |
| `OrcaWhirlpool` | `two_hop_swap` / `two_hop_swap_v2` | route-level | route specified amount | `min_out` or `max_in` | route threshold | route final output or total input |
## Notes
- `Pump` quote side is normalized to `wSOL` in the slippage fields, even when legacy `Swap.QuoteMint` is not populated.
- `OrcaWhirlpool` two-hop instructions use route-level slippage. The normalized slippage fields are attached to the first returned swap entry.
- `MeteoraAmmV2` uses `SwapMode.ExactIn`, `SwapMode.PartialFill`, and `SwapMode.ExactOut`. `PartialFill` is treated like exact-in for slippage purposes because it still uses a minimum-output threshold.
## DAMM v2 Verification
The `MeteoraAmmV2` mapping has been checked against the program IDL for `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG`.
- `swap`
- instruction arg type: `SwapParameters`
- fields: `amountIn`, `minimumAmountOut`
- semantics: exact-in
- `swap2`:
- instruction / event arg type: `SwapParameters2`
- `amount0`: "When it's exact in, partial fill, this will be amount_in. When it's exact out, this will be amount_out"
- `amount1`: "When it's exact in, partial fill, this will be minimum_amount_out. When it's exact out, this will be maximum_amount_in"
- `swapMode`: `ExactIn`, `PartialFill`, `ExactOut`
The downloaded JSON IDL references `SwapMode` in the field docs but does not inline the enum body itself. In this repository, the raw `swapMode` values are interpreted consistently as:
- `0 = ExactIn`
- `1 = PartialFill`
- `2 = ExactOut`
That means the parser mapping is:
- `swap2` + `ExactIn` / `PartialFill`
- `FixedAmount = amount0`
- `LimitAmount = amount1`
- `LimitAmountType = min_out`
- `swap2` + `ExactOut`
- `FixedAmount = amount0`
- `LimitAmount = amount1`
- `LimitAmountType = max_in`
## Source Files
- `Swap` normalized fields: `tx.go`
- Shared slippage mapping helpers: `swap_amounts.go`
- Protocol parsers:
- `pump.go`
- `pumpamm.go`
- `metaoradlmm.go`
- `metaorapool.go`
- `meteora_bonding_curve.go`
- `meteoradamm.go`
- `raydiumlaunchlab.go`
- `raydiumcpmm.go`
- `raydiumclmm.go`
- `raydiumv4.go`
- `orcawhirpool.go`

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

133
cmd/rpc_parse/main.go Normal file
View File

@@ -0,0 +1,133 @@
package main
import (
"context"
"fmt"
"os"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
pump_parser "github.com/thloyi/pump-parser"
)
func main() {
const rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"
txHash := os.Getenv("TX_HASH")
if txHash == "" {
txHash = "2AhpL5KhVmG3D38CwMzrHuRyTucEQ43GzBXL2mo5WiugdZMVmK1dtX8brGe3sxvvFDY6iSSviJTvqCtr4UL3Pc7J"
}
if txHash == "" {
fmt.Fprintln(os.Stderr, "txHash is empty; set it in cmd/rpc_parse/main.go")
os.Exit(1)
}
sig, err := solana.SignatureFromBase58(txHash)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid txHash: %v\n", err)
os.Exit(1)
}
client := rpc.New(rpcURL)
maxSupportedVersion := uint64(0)
out, err := client.GetTransaction(context.Background(), sig, &rpc.GetTransactionOpts{
Encoding: solana.EncodingBase64,
Commitment: rpc.CommitmentConfirmed,
MaxSupportedTransactionVersion: &maxSupportedVersion,
})
if err != nil {
fmt.Fprintf(os.Stderr, "rpc getTransaction error: %v\n", err)
os.Exit(1)
}
if out == nil || out.Transaction == nil || out.Meta == nil {
fmt.Fprintln(os.Stderr, "rpc getTransaction returned empty response")
os.Exit(1)
}
rawBinary := out.Transaction.GetBinary()
if len(rawBinary) == 0 {
fmt.Fprintln(os.Stderr, "rpc getTransaction returned empty transaction data")
os.Exit(1)
}
txWithMeta := rpc.TransactionWithMeta{
Slot: out.Slot,
BlockTime: out.BlockTime,
Transaction: rpc.DataBytesOrJSONFromBytes(rawBinary),
Meta: out.Meta,
Version: out.Version,
}
var blockTime *uint64
if out.BlockTime != nil {
bt := uint64(*out.BlockTime)
blockTime = &bt
}
rawTx, err := pump_parser.FromRpcTransactionWithMeta(txWithMeta, blockTime, out.Slot, 0)
if err != nil {
fmt.Fprintf(os.Stderr, "convert rpc transaction error: %v\n", err)
os.Exit(1)
}
pump_parser.EnableAllParsers()
parsed, err := pump_parser.ParseRawTx(rawTx)
if err != nil {
fmt.Fprintf(os.Stderr, "parse raw tx error: %v\n", err)
os.Exit(1)
}
if len(parsed.Swaps) == 0 {
fmt.Println("no swaps parsed from tx")
return
}
for i, swap := range parsed.Swaps {
fmt.Printf("swap[%d]\n", i)
fmt.Printf(" program: %s\n", swap.Program)
fmt.Printf(" event: %s\n", swap.Event)
fmt.Printf(" pool: %s\n", swap.Pool)
fmt.Printf(" user: %s\n", swap.User)
fmt.Printf(" base_mint: %s (decimals=%d)\n", swap.BaseMint, swap.BaseMintDecimals)
fmt.Printf(" quote_mint: %s (decimals=%d)\n", swap.QuoteMint, swap.QuoteMintDecimals)
fmt.Printf(" base_amount: %s\n", swap.BaseAmount.String())
fmt.Printf(" quote_amount: %s\n", swap.QuoteAmount.String())
if swap.SwapMode != pump_parser.SwapModeUnknown {
fmt.Printf(" swap_mode: %s\n", swap.SwapMode.String())
fmt.Printf(" fixed_amount: %s\n", swap.FixedAmount.String())
fmt.Printf(" fixed_amount_side: %s\n", swap.FixedAmountSide.String())
fmt.Printf(" fixed_mint: %s\n", swap.FixedMint)
fmt.Printf(" limit_amount_type: %s\n", swap.LimitAmountType.String())
fmt.Printf(" limit_amount: %s\n", swap.LimitAmount.String())
fmt.Printf(" limit_amount_side: %s\n", swap.LimitAmountSide.String())
fmt.Printf(" limit_mint: %s\n", swap.LimitMint)
fmt.Printf(" actual_limit_amount: %s\n", swap.ActualLimitAmount.String())
fmt.Printf(" slippage_bps: %s\n", swap.SlippageBps.String())
}
if !swap.FeeAmount.IsZero() || swap.FeeSide != "" {
fmt.Printf(" fee_amount: %s\n", swap.FeeAmount.String())
fmt.Printf(" lp_fee_amount: %s\n", swap.LpFeeAmount.String())
fmt.Printf(" fee_side: %s\n", swap.FeeSide)
fmt.Printf(" fee_mint: %s (decimals=%d)\n", swap.FeeMint, swap.FeeMintDecimals)
fmt.Printf(" fee_token_program: %s\n", swap.FeeTokenProgram)
}
fmt.Printf(" base_reserve: %s\n", swap.BaseReserve.String())
fmt.Printf(" quote_reserve: %s\n", swap.QuoteReserve.String())
fmt.Printf(" base_token_program: %s\n", swap.BaseTokenProgram)
fmt.Printf(" quote_token_program: %s\n", swap.QuoteTokenProgram)
fmt.Printf(" entry_contract: %s\n", swap.EntryContract)
fmt.Printf(" user_base_balance: %s\n", swap.UserBaseBalance.String())
fmt.Printf(" user_quote_balance: %s\n", swap.UserQuoteBalance.String())
fmt.Printf(" active_bin_id: %d\n", swap.ActiveBinId)
fmt.Printf(" start_bin_id: %d\n", swap.StartBinId)
fmt.Printf(" end_bin_id: %d\n", swap.EndBinId)
fmt.Printf(" remove_bp: %d\n", swap.RemoveBp)
fmt.Printf(" position_account: %s\n", swap.PositionAccount)
if swap.Mayhem {
fmt.Printf(" mayhem: true\n")
} else {
fmt.Printf(" mayhem: false\n")
}
fmt.Println()
}
}

View File

@@ -185,6 +185,7 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("soyascXFW5wEEYiwfEmHy2pNwomqzvggJosGVD6TJdY"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("soyasF3QPWPAKKmgA3GjfWax1kmTT1aoqSGxPzVLNUQ"): MevAgentSoyas,
solana.MustPublicKeyFromBase58("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"): MevAgentStellium,
solana.MustPublicKeyFromBase58("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"): MevAgentStellium,
@@ -209,6 +210,14 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("B1ooMauwuJPhHsXqt3uj7B92CAFG8kaD1Q2iGEmGYnx"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMdjcY7zemxDWiH8jVZPxEMdHnE5AraWPHdHQoPj"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("B1ooMKzu6siJzQutP6a6oLiY3fpzgQnBZsAjxuAm9qo"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AstrA1ejL4UeXC2SBP4cpeEmtcFPZVLxx3XGKXyCW6to"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AsTra79FET4aCKWspPqeSFvjJNyp96SvAnrmyAxqg5b7"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AsTRADtvb6tTmrsqULQ9Wji9PigDMjhfEMza6zkynEvV"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AsTRAEoyMofR3vUPpf9k68Gsfb6ymTZttEtsAbv8Bk4d"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AStrAJv2RN2hKCHxwUMtqmSxgdcNZbihCwc1mCSnG83W"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("Astran35aiQUF57XZsmkWMtNCtXGLzs8upfiqXxth2bz"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("AStRAnpi6kFrKypragExgeRoJ1QnKH7pbSjLAKQVWUum"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("ASTRaoF93eYt73TYvwtsv6fMWHWbGmMUZfVZPo3CRU9C"): MevAgentAstralane,
solana.MustPublicKeyFromBase58("Gu2UGEfze3Gg5cHuEC4jGbyCufgpev75RkVvBdKKtf12"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("E8wD3SMD1trozPrvSN9F6SyuUXD7rrFDuR3WexGziKG5"): MevAgent0slot,
solana.MustPublicKeyFromBase58("18hCV7f9CPmZRAH3QCNZaGHhHeNSfisQKeKuFkQsPLY"): MevAgent0slot,
@@ -327,6 +336,51 @@ var mevAgentFeeAddresses = map[solana.PublicKey]string{
solana.MustPublicKeyFromBase58("bgH7YhymSykyvMa3nAZpzvrn73owJHU5iB75S1aiLT9"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("pfngGVVQLiVRFbLWw3Ektiv17ef9NiRZbcgdAhh4ZEW"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("nEFs3jph8HJt7honu3k7XtGUufMnwAvSXmXcKSPxryP"): MevAgentNozomi,
solana.MustPublicKeyFromBase58("Fa1con11xLjPddfzRwRUB16sbFZggp2JeJkCeWREyR8X"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con11TM1RuAQzbQzYjTy4Ekfap9Lnc9fnEbQYEd6Q"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con113Bvi76nS5AzUiRDC2fqjfzkNMUNRLgQybMYt"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con1QGHJK232s8yZpzZZwqPexnAKcoyKj626LNsMv"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con1zUzb6qJVFz5tNkPq1Ahm8H1qKW7Q48252QbkQ"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con16d3MSwd3SAiwvr2LwgkpE7ot8zntbpuec8HAx"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con1i7mpa7Qc6epYJ6r4P9AbU77DFFz173r59Df1x"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con18nWn8TdAGL7JX8PertfMUGVSc899NawokJ4Bq"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con1GKusK2EqsfzrDzGPaYZSxQtFGzJiRMMU9Zm2g"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Fa1con1RDwVwM9VrJ53CwVefD3VU9c58EMpDawV7fLMi"): MevagentFa1con,
solana.MustPublicKeyFromBase58("Sp1x2AqpQckPLaWnWCJUNg8k6qQexfaEWcSRKf5JcDV"): MevagentBlocksprint,
solana.MustPublicKeyFromBase58("Sp4JHSh9cksfzXbgK7Pq2ovtn8LirLQydaJKTsiNT77"): MevagentBlocksprint,
solana.MustPublicKeyFromBase58("Sp1xMS2cbw83SZDNr4AGqkBYYLjb3LvVnmDSrTMaHkr"): MevagentBlocksprint,
solana.MustPublicKeyFromBase58("SpagSJmnh8E9cGT5Y431xPPaS2c1xLREGGCWN9yDeUf"): MevagentBlocksprint,
solana.MustPublicKeyFromBase58("SpWrza9E63MQuHeGnnfzmtLVCs3pBdjyKPXUABPo9nq"): MevagentBlocksprint,
solana.MustPublicKeyFromBase58("moon17L6BgxXRX5uHKudAmqVF96xia9h8ygcmG2sL3F"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moon26Sek222Md7ZydcAGxoKG832DK36CkLrS3PQY4c"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moon7fwyajcVstMoBnVy7UBcTx87SBtNoGGAaH2Cb8V"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonBtH9HvLHjLqi9ivyrMVKgFUsSfrz9BwQ9khhn1u"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonCJg8476LNFLptX1qrK8PdRsA1HD1R6XWyu9MB93"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonF2sz7qwAtdETnrgxNbjonnhGGjd6r4W4UC9284s"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonKfftMiGSak3cezvhEqvkPSzwrmQxQHXuspC96yj"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonQBUKBpkifLcTd78bfxxt4PYLwmJ5admLW6cBBs8"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonXwpKwoVkMegt5Bc776cSW793X1irL5hHV1vJ3JA"): MevAgentMoon,
solana.MustPublicKeyFromBase58("moonZ6u9E2fgk6eWd82621eLPHt9zuJuYECXAYjMY1C"): MevAgentMoon,
solana.MustPublicKeyFromBase58("SpEEdz8S1KorkMZqjMUxfxrmWwofmp6ReNP2Nx6CUmq"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("SpeeDy3GJM4wcrQmk1itRFWgidvxX4rwjTLMv78wwjE"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("SPeEdva37vW8vRtqgYjprQs1g3965icfVN5Rt7SMAyh"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("speEdrSEpox5GUfHWcBc7tQjRuSfUin2yvB7qoYvvJh"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("SPeEDmkHkN3A2roSZf6aZyEMsmrGqTHKqwP51y2Y4rV"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("SpeedLdTJXh2RKpXEaP8JCxkWoUVXhtdPQ1EnxBJMxc"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("SpEediGKLbbXndSYTzwmz6Z3NDgHQLDcTDEvGFkSMH9"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"): MevAgentSpeedlanding,
solana.MustPublicKeyFromBase58("harkEpXoJv5qVzHaN7HSuUAd6PHjyMcFMcDYBMDJCEQ"): MevAgentAllenhark,
solana.MustPublicKeyFromBase58("harkm2BTWxZuszoNpZnfe84jRbQTg6KGHaQBmWzDGQQ"): MevAgentAllenhark,
solana.MustPublicKeyFromBase58("harkR2YJ4Dpt4UDJTcBirjnSPBhNpQFcoFkNpCkVqNk"): MevAgentAllenhark,
solana.MustPublicKeyFromBase58("t3QLYyXH4vZYbEifLqjD581t5dPVhq9LABxWceySzL2"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t46SqGmwStEffUMp1fr2xmv5uyR85TB9annJuLKLf83"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t1TcSg9biJsz4NjKjhopK8QZzPS4KzBgFSszu5QTGgF"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t2XFAFBaUkCzxJwEbLWFX9PKFjfBCp2tSyFtx5z4RZM"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t55hdzzftxWkYy3J8t32C9RRcZDuMZ4LDuBmbTzJFkU"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t6UtTQLUGHJJrzxAb8PBBZdZKra8SWUqvTv9zPnxKNz"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t7qUQU35sLpPydh42BcPmtEfTWW8gBe4Ry3gjwVnokJ"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t8pPgarSK3TnuLbbHmoE1RCQdLxfxuPqNTyFjBKahok"): MevAgentRaiden,
solana.MustPublicKeyFromBase58("t96GGdw3MiaGR993XN8PSsRpKGXx56t5Wf6zcF1hBpY"): MevAgentRaiden,
}
var entryContractAddresses = map[solana.PublicKey]string{

21
enum.go
View File

@@ -15,6 +15,12 @@ const (
MevAgentSoyas = "soyas"
MevAgentStellium = "stellium"
MevAgentAstralane = "astralane"
MevagentFa1con = "fa1con"
MevagentBlocksprint = "blocksprint"
MevAgentMoon = "moon"
MevAgentSpeedlanding = "speedlanding"
MevAgentAllenhark = "allenhark"
MevAgentRaiden = "raiden"
)
const (
@@ -117,5 +123,20 @@ const (
TxEventRemoveLP = "remove"
TxEventBuy = "buy"
TxEventSell = "sell"
TxEventBuyFailed = "buy_failed"
TxEventSellFailed = "sell_failed"
TxEventBurn = "burn"
TxEventCreate = "create"
TxEventComplete = "complete"
TxEventMigrate = "migrate"
TxEventDeposit = "deposit"
TxEventWithdraw = "withdraw"
TxEventOpen = "open"
TxEventClose = "close"
TxEventClaimFee = "claim_fee"
TxEventAddLiquidity = "add_liquidity"
TxEventAddLiquidityOneSide = "add_liquidity_one_side"
TxEventRemoveLiquidity = "remove_liquidity"
TxEventRemoveLiquidityOneSide = "remove_liquidity_one_side"
)

View File

@@ -25,7 +25,7 @@ func main() {
// laserstream-mainnet-slc.helius-rpc.com:80
ch := make(chan example.SubscriptionMessage, 1)
go example.RunLoopWithReConnect(context.Background(), "127.0.0.1:10001", parser.SolProgramPump, ch)
go example.RunLoopWithReConnect(context.Background(), "", "", parser.SolProgramPump, ch)
// var tokenTxs = make(map[string]*types.Tx)
// currentBlock := uint64(0)
for msg := range ch {
@@ -51,9 +51,24 @@ func main() {
//}
// 处理交易
if len(ptx.Swaps) > 0 && (ptx.Swaps[0].Program == parser.SolProgramPump || ptx.Swaps[0].Program == parser.SolProgramPumpAMM) {
fmt.Printf("success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s \n", time.Now().Format("2006-01-02 15:04:05"), ptx.Swaps[0].Program, ptx.Swaps[0].Event, ptx.Block, ptx.GetTxHash(),
ptx.Swaps[0].BaseAmount.Div(decimal.NewFromInt(1e6)), ptx.Swaps[0].QuoteAmount.Div(decimal.NewFromInt(1e9)))
if len(ptx.Swaps) > 0 {
for _, swap := range ptx.Swaps {
if swap.SlippageBps.LessThan(decimal.Zero) || swap.SlippageBps.GreaterThan(decimal.NewFromInt(10000)) {
fmt.Printf("success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s \n", time.Now().Format("2006-01-02 15:04:05"), swap.Program, swap.Event, ptx.Block, ptx.GetTxHash(),
swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9)))
}
if swap.SlippageBps.Equal(decimal.Zero) && (swap.Event == "buy" || swap.Event == "sell") {
fmt.Printf("zero success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s, fix: %s, limit: %s, \n", time.Now().Format("2006-01-02 15:04:05"), swap.Program, swap.Event, ptx.Block, ptx.GetTxHash(),
swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9)), swap.FixedAmount.String(), swap.LimitAmount.String())
}
}
if len(ptx.Swaps) > 0 {
_, err := parser.EncodeTxBinary(ptx)
if err != nil {
fmt.Printf("success tx : %s, , block: %d, tx: %s, err: %s \n", time.Now().Format("2006-01-02 15:04:05"), ptx.Block, ptx.GetTxHash(), err.Error())
}
}
}
// currentBlock = ptx.Block
//

View File

@@ -45,9 +45,11 @@ type Client struct {
firstMessage bool
handler Handler
xToken string
}
func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client {
func NewClientWithPumpSwap(endpoint string, xtoken string, ch chan SubscriptionMessage) *Client {
var subscription pb.SubscribeRequest
//var failed = true
@@ -58,10 +60,10 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client
Vote: &vote,
}
subscription.Transactions["transactions_sub"].AccountInclude = []string{
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
}
//subscription.Transactions["transactions_sub"].AccountInclude = []string{
// "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
// "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
//}
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
@@ -72,6 +74,7 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client
lastReceiveTime: time.Now(),
subStatus: false,
subscription: &subscription,
xToken: xtoken,
}
c.handler = NewPumpHandler(func(tx *types.Tx) {
c.sendTx(tx)
@@ -112,12 +115,12 @@ func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Clien
return c
}
func RunLoopWithReConnect(ctx context.Context, endpoint, program string, ch chan SubscriptionMessage) {
func RunLoopWithReConnect(ctx context.Context, endpoint, token, program string, ch chan SubscriptionMessage) {
var client *Client
if program == types.SolProgramRaydiumLaunchLab {
client = NewClientWithLaunchLab(endpoint, ch)
} else {
client = NewClientWithPumpSwap(endpoint, ch)
client = NewClientWithPumpSwap(endpoint, token, ch)
}
for {
select {
@@ -206,12 +209,13 @@ func (c *Client) grpcSubscribe(ctx context.Context, conn *grpc.ClientConn) error
log.Printf("Subscription request: %s", string(subscriptionJson))
// Set up the subscription request
//if *token != "" {
// md := metadata.New(map[string]string{"x-token": *token})
// ctx = metadata.NewOutgoingContext(ctx, md)
//}
md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"})
if c.xToken != "" {
fmt.Println("xtoken", c.xToken)
md := metadata.New(map[string]string{"x-token": c.xToken})
ctx = metadata.NewOutgoingContext(ctx, md)
}
//md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"})
//ctx = metadata.NewOutgoingContext(ctx, md)
stream, err := client.Subscribe(ctx)
if err != nil {

21
meta.go
View File

@@ -68,7 +68,11 @@ var (
)
var (
meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair2")
meteoraInitializeCustomizablePermissionlessLbPairDiscriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair")
meteoraInitializeCustomizablePermissionlessLbPair2Discriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair2")
meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair")
meteoraInitializeLbPair2Discriminator = calculateDiscriminator("global:initialize_lb_pair2")
meteoraInitializePermissionLbPairDiscriminator = calculateDiscriminator("global:initialize_permission_lb_pair")
meteoraInitializeLbPairEventDiscriminator = calculateDiscriminator("event:LbPairCreate")
meteoraDlmmSwapDiscriminator = calculateDiscriminator("global:swap")
meteoraDlmmSwap2Discriminator = calculateDiscriminator("global:swap2")
@@ -76,14 +80,27 @@ var (
meteoraDlmmSwapExactOut2Discriminator = calculateDiscriminator("global:swap_exact_out2")
meteoraDlmmSwapWithPriceImpactDiscriminator = calculateDiscriminator("global:swap_with_price_impact")
meteoraDlmmSwapWithPriceImpact2Discriminator = calculateDiscriminator("global:swap_with_price_impact2")
meteoraDlmmInitializePositionDiscriminator = calculateDiscriminator("global:initialize_position")
meteoraDlmmInitializePosition2Discriminator = calculateDiscriminator("global:initialize_position2")
meteoraDlmmInitializePositionByOperatorDiscriminator = calculateDiscriminator("global:initialize_position_by_operator")
meteoraDlmmInitializePositionPdaDiscriminator = calculateDiscriminator("global:initialize_position_pda")
meteoraDlmmClosePositionDiscriminator = calculateDiscriminator("global:close_position")
meteoraDlmmClosePosition2Discriminator = calculateDiscriminator("global:close_position2")
meteoraDlmmClosePositionIfEmptyDiscriminator = calculateDiscriminator("global:close_position_if_empty")
meteoraDlmmSwapEventDiscriminator = calculateDiscriminator("event:Swap")
meteoraDlmmAddLiquidityDiscriminator = calculateDiscriminator("global:add_liquidity")
meteoraDlmmAddLiquidity2Discriminator = calculateDiscriminator("global:add_liquidity2")
meteoraDlmmAddLiquidityByStrategyDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy")
meteoraDlmmAddLiquidityByStrategy2Discriminator = calculateDiscriminator("global:add_liquidity_by_strategy2")
meteoraDlmmAddLiquidityByWeightDiscriminator = calculateDiscriminator("global:add_liquidity_by_weight")
meteoraDlmmAddLiquidityOneSideDiscriminator = calculateDiscriminator("global:add_liquidity_one_side")
meteoraDlmmAddLiquidityOneSidePreciseDiscriminator = calculateDiscriminator("global:add_liquidity_one_side_precise")
meteoraDlmmAddLiquidityOneSidePrecise2Discriminator = calculateDiscriminator("global:add_liquidity_one_side_precise2")
meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy_one_side")
meteoraDlmmClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee")
meteoraDlmmClaimFee2Discriminator = calculateDiscriminator("global:claim_fee2")
meteoraDlmmRebalanceLiquidityDiscriminator = calculateDiscriminator("global:rebalance_liquidity")
meteoraDlmmRemoveAllLiquidityDiscriminator = calculateDiscriminator("global:remove_all_liquidity")
meteoraDlmmRemoveLiquidityDiscriminator = calculateDiscriminator("global:remove_liquidity")
meteoraDlmmRemoveLiquidity2Discriminator = calculateDiscriminator("global:remove_liquidity2")
meteoraDlmmRemoveLiquidityByRangeDiscriminator = calculateDiscriminator("global:remove_liquidity_by_range")
@@ -91,6 +108,8 @@ var (
meteoraDlmmAddLiquidityEventDiscriminator = calculateDiscriminator("event:AddLiquidity")
meteoraDlmmClaimFeeEventDiscriminator = calculateDiscriminator("event:ClaimFee")
meteoraDlmmClaimFee2EventDiscriminator = calculateDiscriminator("event:ClaimFee2")
meteoraDlmmPositionCloseEventDiscriminator = calculateDiscriminator("event:PositionClose")
meteoraDlmmPositionCreateEventDiscriminator = calculateDiscriminator("event:PositionCreate")
meteoraDlmmRebalancingEventDiscriminator = calculateDiscriminator("event:Rebalancing")
meteoraDlmmRemoveLiquidityEventDiscriminator = calculateDiscriminator("event:RemoveLiquidity")
)

File diff suppressed because it is too large Load Diff

424
metaoradlmm_test.go Normal file
View File

@@ -0,0 +1,424 @@
package pump_parser
import (
"bytes"
"testing"
agbinary "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func testPublicKey(seed byte) solana.PublicKey {
buf := make([]byte, solana.PublicKeyLength)
for i := range buf {
buf[i] = seed
}
return solana.PublicKeyFromBytes(buf)
}
func seqInts(n int) []int {
out := make([]int, n)
for i := range out {
out[i] = i
}
return out
}
func mustBorshEncode(t *testing.T, value any) []byte {
t.Helper()
var buf bytes.Buffer
if err := agbinary.NewBorshEncoder(&buf).Encode(value); err != nil {
t.Fatalf("borsh encode failed: %v", err)
}
return buf.Bytes()
}
func TestMeteoraDlmmInitializeParserCompatibility(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
discriminator [8]byte
accountCount int
wantPoolPos int
wantBaseMintPos int
wantQuoteMintPos int
wantUserPos int
wantBaseProgramPos int
wantQuoteProgramPos int
}{
{
name: "initialize_lb_pair",
discriminator: meteoraInitializeLbPairDiscriminator,
accountCount: 14,
wantPoolPos: 0,
wantBaseMintPos: 2,
wantQuoteMintPos: 3,
wantUserPos: 8,
wantBaseProgramPos: 9,
wantQuoteProgramPos: 9,
},
{
name: "initialize_lb_pair2",
discriminator: meteoraInitializeLbPair2Discriminator,
accountCount: 16,
wantPoolPos: 0,
wantBaseMintPos: 2,
wantQuoteMintPos: 3,
wantUserPos: 8,
wantBaseProgramPos: 11,
wantQuoteProgramPos: 12,
},
{
name: "initialize_customizable_permissionless_lb_pair",
discriminator: meteoraInitializeCustomizablePermissionlessLbPairDiscriminator,
accountCount: 14,
wantPoolPos: 0,
wantBaseMintPos: 2,
wantQuoteMintPos: 3,
wantUserPos: 8,
wantBaseProgramPos: 9,
wantQuoteProgramPos: 9,
},
{
name: "initialize_customizable_permissionless_lb_pair2",
discriminator: meteoraInitializeCustomizablePermissionlessLbPair2Discriminator,
accountCount: 17,
wantPoolPos: 0,
wantBaseMintPos: 2,
wantQuoteMintPos: 3,
wantUserPos: 8,
wantBaseProgramPos: 11,
wantQuoteProgramPos: 12,
},
{
name: "initialize_permission_lb_pair",
discriminator: meteoraInitializePermissionLbPairDiscriminator,
accountCount: 17,
wantPoolPos: 1,
wantBaseMintPos: 3,
wantQuoteMintPos: 4,
wantUserPos: 8,
wantBaseProgramPos: 11,
wantQuoteProgramPos: 12,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
accountList := make([]solana.PublicKey, 32)
for i := range accountList {
accountList[i] = testPublicKey(byte(i + 1))
}
programIndex := 30
accountList[programIndex] = meteoraDlmmProgram
instruction := Instruction{
Accounts: seqInts(tc.accountCount),
Data: solana.Base58(tc.discriminator[:]),
ProgramIDIndex: programIndex,
}
rawTx := &RawTx{
accountList: accountList,
Meta: Meta{
PostTokenBalances: []TokenBalance{
{
MintAccount: accountList[tc.wantBaseMintPos],
UITokenAmount: UITokenAmount{
Decimals: 6,
},
},
{
MintAccount: accountList[tc.wantQuoteMintPos],
UITokenAmount: UITokenAmount{
Decimals: 9,
},
},
},
},
Transaction: Transaction{
Message: Message{
Instructions: []Instruction{instruction},
},
},
}
tx := &Tx{rawTx: rawTx}
swaps, _, err := metaoradlmmParser(tx, instruction, InnerInstructions{}, [2]uint{0, 0})
if err != nil {
t.Fatalf("metaoradlmmParser() error = %v", err)
}
if len(swaps) != 1 {
t.Fatalf("metaoradlmmParser() swaps len = %d, want 1", len(swaps))
}
swap := swaps[0]
if !swap.Pool.Equals(accountList[tc.wantPoolPos]) {
t.Fatalf("swap.Pool = %s, want %s", swap.Pool, accountList[tc.wantPoolPos])
}
if !swap.BaseMint.Equals(accountList[tc.wantBaseMintPos]) {
t.Fatalf("swap.BaseMint = %s, want %s", swap.BaseMint, accountList[tc.wantBaseMintPos])
}
if !swap.QuoteMint.Equals(accountList[tc.wantQuoteMintPos]) {
t.Fatalf("swap.QuoteMint = %s, want %s", swap.QuoteMint, accountList[tc.wantQuoteMintPos])
}
if !swap.User.Equals(accountList[tc.wantUserPos]) {
t.Fatalf("swap.User = %s, want %s", swap.User, accountList[tc.wantUserPos])
}
if !swap.BaseTokenProgram.Equals(accountList[tc.wantBaseProgramPos]) {
t.Fatalf("swap.BaseTokenProgram = %s, want %s", swap.BaseTokenProgram, accountList[tc.wantBaseProgramPos])
}
if !swap.QuoteTokenProgram.Equals(accountList[tc.wantQuoteProgramPos]) {
t.Fatalf("swap.QuoteTokenProgram = %s, want %s", swap.QuoteTokenProgram, accountList[tc.wantQuoteProgramPos])
}
if swap.BaseMintDecimals != 6 {
t.Fatalf("swap.BaseMintDecimals = %d, want 6", swap.BaseMintDecimals)
}
if swap.QuoteMintDecimals != 9 {
t.Fatalf("swap.QuoteMintDecimals = %d, want 9", swap.QuoteMintDecimals)
}
if !swap.EntryContract.Equals(meteoraDlmmProgram) {
t.Fatalf("swap.EntryContract = %s, want %s", swap.EntryContract, meteoraDlmmProgram)
}
})
}
}
func TestDlmmDecodeLbPairCreateEvent(t *testing.T) {
t.Parallel()
event := dlmmLbPairCreateEvent{
LbPair: testPublicKey(90),
BinStep: 42,
TokenX: testPublicKey(91),
TokenY: testPublicKey(92),
}
body := mustBorshEncode(t, event)
barePayload := append(append([]byte{}, meteoraInitializeLbPairEventDiscriminator[:]...), body...)
decodedBare, ok := dlmmDecodeLbPairCreateEvent(barePayload)
if !ok {
t.Fatalf("dlmmDecodeLbPairCreateEvent() failed for bare payload")
}
if decodedBare != event {
t.Fatalf("decoded bare event = %+v, want %+v", decodedBare, event)
}
anchorPayload := append(append(append([]byte{}, eventDiscriminator[:]...), meteoraInitializeLbPairEventDiscriminator[:]...), body...)
decodedAnchor, ok := dlmmDecodeLbPairCreateEvent(anchorPayload)
if !ok {
t.Fatalf("dlmmDecodeLbPairCreateEvent() failed for anchor payload")
}
if decodedAnchor != event {
t.Fatalf("decoded anchor event = %+v, want %+v", decodedAnchor, event)
}
}
func TestMeteoraDlmmInitializeParserUsesLbPairCreateEvent(t *testing.T) {
t.Parallel()
accountList := make([]solana.PublicKey, 32)
for i := range accountList {
accountList[i] = testPublicKey(byte(i + 1))
}
programIndex := 30
accountList[programIndex] = meteoraDlmmProgram
instruction := Instruction{
Accounts: seqInts(16),
Data: solana.Base58(meteoraInitializeLbPair2Discriminator[:]),
ProgramIDIndex: programIndex,
}
event := dlmmLbPairCreateEvent{
LbPair: testPublicKey(111),
BinStep: 25,
TokenX: testPublicKey(112),
TokenY: testPublicKey(113),
}
innerEventData := append(
append(append([]byte{}, eventDiscriminator[:]...), meteoraInitializeLbPairEventDiscriminator[:]...),
mustBorshEncode(t, event)...,
)
rawTx := &RawTx{
accountList: accountList,
Meta: Meta{
PostTokenBalances: []TokenBalance{
{
MintAccount: accountList[2],
UITokenAmount: UITokenAmount{
Decimals: 6,
},
},
{
MintAccount: accountList[3],
UITokenAmount: UITokenAmount{
Decimals: 9,
},
},
},
InnerInstructions: []InnerInstructions{
{
Index: 0,
Instructions: []Instruction{
{
ProgramIDIndex: programIndex,
Data: solana.Base58(innerEventData),
},
},
},
},
},
Transaction: Transaction{
Message: Message{
Instructions: []Instruction{instruction},
},
},
}
tx := &Tx{rawTx: rawTx}
swaps, nextOffset, err := metaoradlmmParser(tx, instruction, rawTx.Meta.InnerInstructions[0], [2]uint{0, 0})
if err != nil {
t.Fatalf("metaoradlmmParser() error = %v", err)
}
if len(swaps) != 1 {
t.Fatalf("metaoradlmmParser() swaps len = %d, want 1", len(swaps))
}
swap := swaps[0]
if !swap.Pool.Equals(event.LbPair) {
t.Fatalf("swap.Pool = %s, want event %s", swap.Pool, event.LbPair)
}
if !swap.BaseMint.Equals(event.TokenX) {
t.Fatalf("swap.BaseMint = %s, want event %s", swap.BaseMint, event.TokenX)
}
if !swap.QuoteMint.Equals(event.TokenY) {
t.Fatalf("swap.QuoteMint = %s, want event %s", swap.QuoteMint, event.TokenY)
}
if nextOffset != ([2]uint{1, 0}) {
t.Fatalf("nextOffset = %#v, want [2]uint{1, 0}", nextOffset)
}
}
func TestDlmmSwapFeeInfo(t *testing.T) {
t.Parallel()
baseMint := testPublicKey(1)
quoteMint := testPublicKey(2)
baseProgram := testPublicKey(3)
quoteProgram := testPublicKey(4)
testCases := []struct {
name string
baseIsX bool
swapForY bool
wantFeeSide string
wantFeeMint solana.PublicKey
wantFeeProg solana.PublicKey
wantDecimals uint8
}{
{
name: "x is base and input is x",
baseIsX: true,
swapForY: true,
wantFeeSide: "base",
wantFeeMint: baseMint,
wantFeeProg: baseProgram,
wantDecimals: 6,
},
{
name: "x is base and input is y",
baseIsX: true,
swapForY: false,
wantFeeSide: "quote",
wantFeeMint: quoteMint,
wantFeeProg: quoteProgram,
wantDecimals: 9,
},
{
name: "y is base and input is x",
baseIsX: false,
swapForY: true,
wantFeeSide: "quote",
wantFeeMint: quoteMint,
wantFeeProg: quoteProgram,
wantDecimals: 9,
},
{
name: "y is base and input is y",
baseIsX: false,
swapForY: false,
wantFeeSide: "base",
wantFeeMint: baseMint,
wantFeeProg: baseProgram,
wantDecimals: 6,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
feeAmount, feeSide, feeMint, feeProgram, feeDecimals := dlmmSwapFeeInfo(
tc.baseIsX,
tc.swapForY,
123,
baseMint,
quoteMint,
baseProgram,
quoteProgram,
6,
9,
)
if !feeAmount.Equal(decimal.NewFromInt(123)) {
t.Fatalf("feeAmount = %s, want 123", feeAmount)
}
if feeSide != tc.wantFeeSide {
t.Fatalf("feeSide = %s, want %s", feeSide, tc.wantFeeSide)
}
if !feeMint.Equals(tc.wantFeeMint) {
t.Fatalf("feeMint = %s, want %s", feeMint, tc.wantFeeMint)
}
if !feeProgram.Equals(tc.wantFeeProg) {
t.Fatalf("feeProgram = %s, want %s", feeProgram, tc.wantFeeProg)
}
if feeDecimals != tc.wantDecimals {
t.Fatalf("feeDecimals = %d, want %d", feeDecimals, tc.wantDecimals)
}
})
}
}
func TestDlmmSwapLpFeeAmount(t *testing.T) {
t.Parallel()
lpFee := dlmmSwapLpFeeAmount(100, 15, 5)
if !lpFee.Equal(decimal.NewFromInt(80)) {
t.Fatalf("lpFee = %s, want 80", lpFee)
}
lpFee = dlmmSwapLpFeeAmount(10, 8, 5)
if !lpFee.IsZero() {
t.Fatalf("lpFee should floor at zero, got %s", lpFee)
}
}
func TestDlmmSwapFeeBpsString(t *testing.T) {
t.Parallel()
feeBps := agbinary.Uint128{Lo: 12345}
if got := dlmmSwapFeeBpsString(feeBps); got != "12345" {
t.Fatalf("dlmmSwapFeeBpsString() = %s, want 12345", got)
}
}

View File

@@ -15,6 +15,11 @@ type metaoraPoolInitializePoolData struct {
TokenBAmount uint64 `json:"tokenBAmount"`
}
type metaoraPoolSwapArgs struct {
InAmount uint64
MinimumOutAmount uint64
}
var (
meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi")
meteoraVaultDepositDiscriminator = []byte{0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6}
@@ -726,6 +731,10 @@ func metaoraPoolRemoveLiquidity(tx *Tx, instruction Instruction, innerInstructio
}
func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
var args metaoraPoolSwapArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode meteora pools swap args: %w", err)
}
pool := tx.rawTx.accountList[instruction.Accounts[0]]
payer := tx.rawTx.accountList[instruction.Accounts[12]]
@@ -874,5 +883,10 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
EntryContract: entryContract,
},
}
swaps[0].SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(args.InAmount),
decimal.NewFromUint64(args.MinimumOutAmount),
)
return swaps, offset, nil
}

View File

@@ -385,5 +385,12 @@ func metaBcSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerIn
EntryContract: entryContract,
},
}
if swapEvent.Params != nil {
swaps[0].SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(swapEvent.Params.AmountIn),
decimal.NewFromUint64(swapEvent.Params.MinimumAmountOut),
)
}
return swaps, offset, nil
}

View File

@@ -188,6 +188,36 @@ type meteoraDammSwapEvent struct {
ReserveBAmount uint64
}
func meteoraDammSwapAmountInfo(event string, params *struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
_ = event
if params == nil {
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
// Meteora DAMM v2 IDL defines:
// - swap: SwapParameters{ amountIn, minimumAmountOut }
// - swap2: SwapParameters2{ amount0, amount1, swapMode }
// - ExactIn / PartialFill: amount0=amount_in, amount1=minimum_amount_out
// - ExactOut: amount0=amount_out, amount1=maximum_amount_in
//
// `SetSwapAmountInfo` derives sides from the normalized buy/sell event, so
// the instruction parameters should stay in raw IDL order here.
switch params.SwapMode {
case 0, 1: // ExactIn / PartialFill
swapMode = SwapModeExactIn
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
case 2: // ExactOut
swapMode = SwapModeExactOut
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
default:
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
}
func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 9 {
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
@@ -276,8 +306,7 @@ func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerI
return nil, offset, fmt.Errorf("invalid trade direction")
}
return []Swap{
{
swap := Swap{
Program: SolProgramMeteoraAmmV2,
Event: event,
Pool: swapEvent.Pool,
@@ -296,8 +325,11 @@ func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerI
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
if swapMode, fixedAmount, limitAmount, ok := meteoraDammSwapAmountInfo(event, swapEvent.Params); ok {
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
}
return []Swap{swap}, offset, nil
}

View File

@@ -1,12 +1,33 @@
package pump_parser
import (
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func decodeOrcaWhirlpoolSwapArgs(data []byte) (amount uint64, otherAmountThreshold uint64, amountSpecifiedIsInput bool, err error) {
if len(data) < 42 {
return 0, 0, false, fmt.Errorf("orca whirlpool swap instruction data too short")
}
amount = binary.LittleEndian.Uint64(data[8:16])
otherAmountThreshold = binary.LittleEndian.Uint64(data[16:24])
amountSpecifiedIsInput = data[40] != 0
return amount, otherAmountThreshold, amountSpecifiedIsInput, nil
}
func decodeOrcaWhirlpoolTwoHopSwapArgs(data []byte) (amount uint64, otherAmountThreshold uint64, amountSpecifiedIsInput bool, err error) {
if len(data) < 27 {
return 0, 0, false, fmt.Errorf("orca whirlpool two-hop swap instruction data too short")
}
amount = binary.LittleEndian.Uint64(data[8:16])
otherAmountThreshold = binary.LittleEndian.Uint64(data[16:24])
amountSpecifiedIsInput = data[24] != 0
return amount, otherAmountThreshold, amountSpecifiedIsInput, nil
}
func orcaWhirPoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(orcaProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("orcawhirpoolprogram instruction not found, offset, %d, %d", offset[0], offset[1])
@@ -709,6 +730,14 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amount, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolSwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
swapMode := SwapModeExactOut
if amountSpecifiedIsInput {
swapMode = SwapModeExactIn
}
user := tx.rawTx.accountList[instruction.Accounts[1]]
pool := tx.rawTx.accountList[instruction.Accounts[2]]
@@ -781,8 +810,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
return nil, offset, fmt.Errorf("orca whirpool swap failed to find both base and quote token transfer in inner instructions")
}
return []Swap{
{
swap := Swap{
Program: SolProgramOrcaWhirPool,
Event: event,
Pool: pool,
@@ -800,8 +828,10 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
UserQuoteBalance: userQuote,
User: user,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}
func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -810,6 +840,14 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amount, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolSwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
swapMode := SwapModeExactOut
if amountSpecifiedIsInput {
swapMode = SwapModeExactIn
}
user := tx.rawTx.accountList[instruction.Accounts[3]]
pool := tx.rawTx.accountList[instruction.Accounts[4]]
@@ -883,8 +921,7 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
}
offset[1] += uint(skipOffset + 1)
return []Swap{
{
swap := Swap{
Program: SolProgramOrcaWhirPool,
Event: event,
Pool: pool,
@@ -902,8 +939,10 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
UserQuoteBalance: userQuote,
User: user,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}
func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -912,6 +951,14 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolTwoHopSwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
swapMode := SwapModeExactOut
if amountSpecifiedIsInput {
swapMode = SwapModeExactIn
}
user := tx.rawTx.accountList[instruction.Accounts[1]]
pool1 := tx.rawTx.accountList[instruction.Accounts[2]]
@@ -1082,6 +1129,29 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
EntryContract: entryContract,
}
}
fixedSide := fixedSwapAmountSide(swaps[0].Event, swapMode)
fixedMint := swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, fixedSide)
limitSide := oppositeSwapAmountSide(fixedSide)
limitMint := swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, limitSide)
actualLimitAmount := swapAmountForSide(swaps[1].BaseAmount, swaps[1].QuoteAmount, limitSide)
if swapMode == SwapModeExactOut {
fixedSide = fixedSwapAmountSide(swaps[1].Event, swapMode)
fixedMint = swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, fixedSide)
limitSide = oppositeSwapAmountSide(fixedSwapAmountSide(swaps[0].Event, swapMode))
limitMint = swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, limitSide)
actualLimitAmount = swapAmountForSide(swaps[0].BaseAmount, swaps[0].QuoteAmount, limitSide)
}
swaps[0].SetSwapAmountInfoDetailed(
swapMode,
decimal.NewFromUint64(amountSpecified),
fixedSide,
fixedMint,
limitSwapAmountType(swapMode),
decimal.NewFromUint64(otherAmountThreshold),
limitSide,
limitMint,
actualLimitAmount,
)
return swaps, offset, nil
}
@@ -1091,6 +1161,14 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, amountSpecifiedIsInput, err := decodeOrcaWhirlpoolTwoHopSwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
swapMode := SwapModeExactOut
if amountSpecifiedIsInput {
swapMode = SwapModeExactIn
}
user := tx.rawTx.accountList[instruction.Accounts[14]]
pool1 := tx.rawTx.accountList[instruction.Accounts[0]]
@@ -1258,5 +1336,28 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
EntryContract: entryContract,
}
}
fixedSide := fixedSwapAmountSide(swaps[0].Event, swapMode)
fixedMint := swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, fixedSide)
limitSide := oppositeSwapAmountSide(fixedSide)
limitMint := swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, limitSide)
actualLimitAmount := swapAmountForSide(swaps[1].BaseAmount, swaps[1].QuoteAmount, limitSide)
if swapMode == SwapModeExactOut {
fixedSide = fixedSwapAmountSide(swaps[1].Event, swapMode)
fixedMint = swapMintForSide(swaps[1].BaseMint, swaps[1].QuoteMint, fixedSide)
limitSide = oppositeSwapAmountSide(fixedSwapAmountSide(swaps[0].Event, swapMode))
limitMint = swapMintForSide(swaps[0].BaseMint, swaps[0].QuoteMint, limitSide)
actualLimitAmount = swapAmountForSide(swaps[0].BaseAmount, swaps[0].QuoteAmount, limitSide)
}
swaps[0].SetSwapAmountInfoDetailed(
swapMode,
decimal.NewFromUint64(amountSpecified),
fixedSide,
fixedMint,
limitSwapAmountType(swapMode),
decimal.NewFromUint64(otherAmountThreshold),
limitSide,
limitMint,
actualLimitAmount,
)
return swaps, offset, nil
}

36
pump.go
View File

@@ -218,6 +218,31 @@ type PumpTradeArgs struct {
Amount2 uint64
}
func pumpTradeAmountInfoFromArgs(args PumpTradeArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
switch {
case bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]):
return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
default:
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
}
func normalizePumpQuoteSideMint(s *Swap) {
if s.FixedAmountSide == SwapAmountSideQuote && s.FixedMint.IsZero() {
s.FixedMint = wSolMint
}
if s.LimitAmountSide == SwapAmountSideQuote && s.LimitMint.IsZero() {
s.LimitMint = wSolMint
}
if s.ActualLimitAmountSide == SwapAmountSideQuote && s.LimitMint.IsZero() {
s.LimitMint = wSolMint
}
}
func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if tx.Err == nil || tx.Err.UnKnown != "" {
return nil, increaseOffset(offset), fmt.Errorf("tx pump failed but error is nil, offset, %d, %d", offset[0], offset[1])
@@ -315,6 +340,10 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
EntryContract: entryContract,
},
}
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
normalizePumpQuoteSideMint(&swaps[0])
}
return swaps, offset, nil
}
@@ -466,6 +495,13 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
Cashback: isCashbackCoin,
},
}
var args PumpTradeArgs
if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err == nil {
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
normalizePumpQuoteSideMint(&swaps[0])
}
}
if completed {
swaps = append(swaps, Swap{
Program: SolProgramPump,

View File

@@ -11,6 +11,31 @@ import (
"github.com/mr-tron/base58"
)
type legacyPumpTradeEvent struct {
Mint solana.PublicKey
SolAmount uint64
TokenAmount uint64
IsBuy bool
User solana.PublicKey
Timestamp int64
VirtualSolReserves uint64
VirtualTokenReserves uint64
RealSolReserves uint64
RealTokenReserves uint64
FeeRecipient solana.PublicKey
FeeBasisPoints uint64
Fee uint64
Creator solana.PublicKey
CreatorFeeBasisPoints uint64
CreatorFee uint64
TrackVolume bool
TotalUnclaimedTokens uint64
TotalClaimedTokens uint64
CurrentSolVolume uint64
LastUpdateTimestamp int64
IxName string
}
func TestTradeEvent(t *testing.T) {
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
d, err := hex.DecodeString(hexData)
@@ -18,13 +43,21 @@ func TestTradeEvent(t *testing.T) {
t.Errorf("Failed to decode base64 data: %v", err)
}
var tradeEvent PumpTradeEvent
var tradeEvent legacyPumpTradeEvent
err = agbinary.NewBorshDecoder(d[16:]).Decode(&tradeEvent)
if err != nil {
t.Errorf("Failed to deserialize trade event: %v", err)
t.Fatalf("Failed to deserialize trade event: %v", err)
}
if tradeEvent.IxName != "buy_exact_sol_in" {
t.Fatalf("IxName = %q, want buy_exact_sol_in", tradeEvent.IxName)
}
if tradeEvent.SolAmount != 11725956 {
t.Fatalf("SolAmount = %d, want 11725956", tradeEvent.SolAmount)
}
if !tradeEvent.IsBuy {
t.Fatalf("IsBuy = false, want true")
}
t.Logf("Trade Event: %+v", tradeEvent)
xx, err := base58.Decode("3Bxs48EzTZB4tzRd")

View File

@@ -261,6 +261,19 @@ type PumpSwapArgs struct {
Amount2 uint64
}
func pumpAmmSwapAmountInfoFromArgs(args PumpSwapArgs) (swapMode SwapMode, fixedAmount decimal.Decimal, limitAmount decimal.Decimal, ok bool) {
switch {
case bytes.Equal(args.Discriminator[:], pumpAmmBuyV2Discriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpAmmBuyDiscriminator[:]):
return SwapModeExactOut, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
case bytes.Equal(args.Discriminator[:], pumpAmmSellDiscriminator[:]):
return SwapModeExactIn, decimal.NewFromUint64(args.Amount1), decimal.NewFromUint64(args.Amount2), true
default:
return SwapModeUnknown, decimal.Zero, decimal.Zero, false
}
}
func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if tx.Err == nil || tx.Err.UnKnown != "" {
return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
@@ -361,8 +374,7 @@ func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions Inn
}
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
return []Swap{
{
swap := Swap{
Program: SolProgramPumpAMM,
Event: event,
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
@@ -381,8 +393,11 @@ func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions Inn
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
if swapMode, fixedAmount, limitAmount, ok := pumpAmmSwapAmountInfoFromArgs(args); ok {
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
}
return []Swap{swap}, offset, nil
}
func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -479,8 +494,7 @@ func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions In
}
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
return []Swap{
{
swap := Swap{
Program: SolProgramPumpAMM,
Event: event,
Pool: tx.rawTx.accountList[instruction.Accounts[0]],
@@ -499,8 +513,11 @@ func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions In
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
if swapMode, fixedAmount, limitAmount, ok := pumpAmmSwapAmountInfoFromArgs(args); ok {
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
}
return []Swap{swap}, offset, nil
}
func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -599,8 +616,7 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
}
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
return []Swap{
{
swap := Swap{
Program: SolProgramPumpAMM,
Event: "buy",
Pool: event.Pool,
@@ -621,8 +637,21 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
if bytes.Equal(instruction.Data[:8], pumpAmmBuyV2Discriminator[:]) {
swap.SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(event.UserQuoteAmountIn),
decimal.NewFromUint64(event.MinBaseAmountOut),
)
} else {
swap.SetSwapAmountInfo(
SwapModeExactOut,
decimal.NewFromUint64(event.BaseAmountOut),
decimal.NewFromUint64(event.MaxQuoteAmountIn),
)
}
return []Swap{swap}, offset, nil
}
func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -722,8 +751,7 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
}
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
return []Swap{
{
swap := Swap{
Program: SolProgramPumpAMM,
Event: "sell",
Pool: event.Pool,
@@ -744,8 +772,13 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(event.BaseAmountIn),
decimal.NewFromUint64(event.MinQuoteAmountOut),
)
return []Swap{swap}, offset, nil
}
func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {

View File

@@ -872,7 +872,9 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT
}
sTx.Meta.Fee = meta.Fee
//sTx.Meta.InnerInstructions = meta.InnerInstructions
if meta.ComputeUnitsConsumed != nil {
sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed
}
for _, innerInstr := range meta.InnerInstructions {
var instrs []Instruction
for _, instr := range innerInstr.Instructions {

View File

@@ -1,12 +1,27 @@
package pump_parser
import (
"encoding/binary"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func decodeRaydiumClmmSwapArgs(data []byte) (amountSpecified uint64, otherAmountThreshold uint64, swapMode SwapMode, err error) {
if len(data) < 41 {
return 0, 0, SwapModeUnknown, fmt.Errorf("raydium clmm swap instruction data too short")
}
amountSpecified = binary.LittleEndian.Uint64(data[8:16])
otherAmountThreshold = binary.LittleEndian.Uint64(data[16:24])
isBaseInput := data[40] != 0
swapMode = SwapModeExactOut
if isBaseInput {
swapMode = SwapModeExactIn
}
return amountSpecified, otherAmountThreshold, swapMode, nil
}
func raydiumClmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumClmmProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("raydiumClmm instruction not found, offset, %d, %d", offset[0], offset[1])
@@ -278,6 +293,10 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
userTokenOutAccount int
)
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumClmmSwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
if discriminator == raydiumClmmSwapDiscriminator {
accountMin = 9
pool = tx.rawTx.accountList[instruction.Accounts[2]]
@@ -350,8 +369,7 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
offset[1] += 2
return []Swap{
{
swap := Swap{
Program: SolProgramRaydiumCLMM,
Event: "sell",
Pool: pool,
@@ -369,7 +387,8 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}

View File

@@ -4,9 +4,20 @@ import (
"bytes"
"fmt"
agbinary "github.com/gagliardetto/binary"
"github.com/shopspring/decimal"
)
type raydiumCPmmSwapBaseInputArgs struct {
AmountIn uint64
MinimumAmountOut uint64
}
type raydiumCPmmSwapBaseOutputArgs struct {
MaxAmountIn uint64
AmountOut uint64
}
func raydiumCPmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumCPmmProgramID) {
return nil, increaseOffset(offset), fmt.Errorf("raydiumCPmm instruction not found, offset, %d, %d", offset[0], offset[1])
@@ -327,6 +338,30 @@ func raydiumCPmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
discriminator := *(*[8]byte)(instruction.Data[:8])
var swapMode SwapMode
var fixedAmount decimal.Decimal
var limitAmount decimal.Decimal
switch discriminator {
case raydiumCPmmSwapBaseInputDiscriminator:
var args raydiumCPmmSwapBaseInputArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode raydium cpmm swap_base_input args: %w", err)
}
swapMode = SwapModeExactIn
fixedAmount = decimal.NewFromUint64(args.AmountIn)
limitAmount = decimal.NewFromUint64(args.MinimumAmountOut)
case raydiumCPmmSwapBaseOutputDiscriminator:
var args raydiumCPmmSwapBaseOutputArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode raydium cpmm swap_base_output args: %w", err)
}
swapMode = SwapModeExactOut
fixedAmount = decimal.NewFromUint64(args.AmountOut)
limitAmount = decimal.NewFromUint64(args.MaxAmountIn)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
market := tx.rawTx.accountList[instruction.Accounts[3]]
// Get token accounts from instruction
tokenIn := tx.rawTx.accountList[instruction.Accounts[4]]
@@ -384,8 +419,7 @@ func raydiumCPmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions")
}
offset[1] += 2
return []Swap{
{
swap := Swap{
Program: SolProgramRaydiumCPMM,
Event: "sell",
Pool: market,
@@ -403,6 +437,8 @@ func raydiumCPmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
return []Swap{swap}, offset, nil
}

View File

@@ -367,6 +367,11 @@ type RaydiumLaunchLabSwapEvent struct {
}
type raydiumLaunchLabSwapArgs struct {
Amount uint64
OtherAmountThreshold uint64
}
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
var programName string
@@ -375,6 +380,26 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
} else {
programName = SolProgramRaydiumLaunchLab
}
discriminator := *(*[8]byte)(instruction.Data[:8])
var swapMode SwapMode
var fixedAmount decimal.Decimal
var limitAmount decimal.Decimal
var swapArgs raydiumLaunchLabSwapArgs
if err := agbinary.NewBorshDecoder(instruction.Data[8:]).Decode(&swapArgs); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed to decode raydium launchlab swap args: %w", err)
}
switch discriminator {
case raydiumLaunchLabSellExactInDiscriminator, raydiumLaunchLabBuyExactInDiscriminator:
swapMode = SwapModeExactIn
fixedAmount = decimal.NewFromUint64(swapArgs.Amount)
limitAmount = decimal.NewFromUint64(swapArgs.OtherAmountThreshold)
case raydiumLaunchLabSellExactOutDiscriminator, raydiumLaunchLabBuyExactOutDiscriminator:
swapMode = SwapModeExactOut
fixedAmount = decimal.NewFromUint64(swapArgs.Amount)
limitAmount = decimal.NewFromUint64(swapArgs.OtherAmountThreshold)
default:
return nil, increaseOffset(offset), InstructionIgnoredError
}
var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
user := tx.rawTx.accountList[instruction.Accounts[0]]
pool := tx.rawTx.accountList[instruction.Accounts[4]]
@@ -447,7 +472,7 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
userBase := getAccountBalanceAfterTx(tx.rawTx, userBaseIdx)
userQuote := getAccountBalanceAfterTx(tx.rawTx, userQuoteIdx)
return []Swap{{
swap := Swap{
Program: programName,
Event: event,
Pool: pool,
@@ -466,5 +491,8 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
}}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
return []Swap{swap}, offset, nil
}

View File

@@ -1,11 +1,26 @@
package pump_parser
import (
"encoding/binary"
"fmt"
"github.com/shopspring/decimal"
)
func decodeRaydiumV4SwapArgs(data []byte) (amountSpecified uint64, otherAmountThreshold uint64, swapMode SwapMode, err error) {
if len(data) < 17 {
return 0, 0, SwapModeUnknown, fmt.Errorf("raydium v4 swap instruction data too short")
}
switch data[0] {
case raydiumV4SwapBaseInDiscriminator, raydiumV4SwapBaseInV2Discriminator:
return binary.LittleEndian.Uint64(data[1:9]), binary.LittleEndian.Uint64(data[9:17]), SwapModeExactIn, nil
case raydiumV4SwapBaseOutDiscriminator, raydiumV4SwapBaseOutV2Discriminator:
return binary.LittleEndian.Uint64(data[9:17]), binary.LittleEndian.Uint64(data[1:9]), SwapModeExactOut, nil
default:
return 0, 0, SwapModeUnknown, InstructionIgnoredError
}
}
func raydiumV4Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumV4Program) {
return nil, increaseOffset(offset), fmt.Errorf("raydiumv4 instruction not found, offset, %d, %d", offset[0], offset[1])
@@ -314,6 +329,10 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne
vaultQuoteIdx = instruction.Accounts[6]
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumV4SwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
@@ -376,8 +395,7 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
return []Swap{
{
swap := Swap{
Program: SolProgramRaydiumV4,
Event: event,
Pool: ammAccount,
@@ -396,17 +414,25 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}
func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
accountsLen := len(instruction.Accounts)
if accountsLen != 8 {
if accountsLen < 8 {
return nil, increaseOffset(offset), fmt.Errorf("invalid number of accounts for raydiumv4 swapv2 instruction, offset %d, %d", offset[0], offset[1])
}
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
amountSpecified, otherAmountThreshold, swapMode, err := decodeRaydiumV4SwapArgs(instruction.Data)
if err != nil {
return nil, increaseOffset(offset), err
}
// Raydium's documented V2 layout uses the first 8 accounts. Routed CPI calls
// may append extra readonly accounts (for example the Raydium program id) at
// the tail, so we only require the canonical prefix here.
ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
user := tx.rawTx.accountList[instruction.Accounts[7]]
userSourceTokenAccount := tx.rawTx.accountList[instruction.Accounts[5]]
@@ -472,8 +498,7 @@ func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions In
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
return []Swap{
{
swap := Swap{
Program: SolProgramRaydiumV4,
Event: event,
Pool: ammAccount,
@@ -492,6 +517,7 @@ func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions In
UserBaseBalance: userBase,
UserQuoteBalance: userQuote,
EntryContract: entryContract,
},
}, offset, nil
}
swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
}

144
raydiumv4_test.go Normal file
View File

@@ -0,0 +1,144 @@
package pump_parser
import (
"encoding/binary"
"testing"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func transferInstructionData(amount uint64) solana.Base58 {
data := make([]byte, 9)
data[0] = 3
binary.LittleEndian.PutUint64(data[1:], amount)
return solana.Base58(data)
}
func raydiumV4SwapInstructionData(discriminator byte, amountSpecified, otherAmountThreshold uint64) solana.Base58 {
data := make([]byte, 17)
data[0] = discriminator
binary.LittleEndian.PutUint64(data[1:9], amountSpecified)
binary.LittleEndian.PutUint64(data[9:17], otherAmountThreshold)
return solana.Base58(data)
}
func TestRaydiumV4SwapV2ParserAllowsTrailingReadonlyAccounts(t *testing.T) {
t.Parallel()
accountList := make([]solana.PublicKey, 32)
for i := range accountList {
accountList[i] = testPublicKey(byte(i + 1))
}
accountList[0] = solana.TokenProgramID
accountList[8] = raydiumV4Program
accountList[20] = testPublicKey(200)
accountList[21] = testPublicKey(201)
accountList[22] = testPublicKey(202)
outerInstruction := Instruction{ProgramIDIndex: 20}
swapInstruction := Instruction{
Accounts: []int{0, 1, 2, 3, 4, 5, 6, 7, 8},
ProgramIDIndex: 8,
Data: raydiumV4SwapInstructionData(raydiumV4SwapBaseInV2Discriminator, 55, 42),
}
innerInstructions := InnerInstructions{
Index: 0,
Instructions: []Instruction{
swapInstruction,
{
Accounts: []int{5, 4, 7},
ProgramIDIndex: 0,
Data: transferInstructionData(55),
},
{
Accounts: []int{3, 6, 2},
ProgramIDIndex: 0,
Data: transferInstructionData(42),
},
},
}
rawTx := &RawTx{
accountList: accountList,
Meta: Meta{
PostTokenBalances: []TokenBalance{
{
AccountIndex: 3,
MintAccount: accountList[21],
ProgramIDAccount: solana.TokenProgramID,
UITokenAmount: UITokenAmount{
Amount: "1000",
Decimals: 6,
},
},
{
AccountIndex: 4,
MintAccount: accountList[22],
ProgramIDAccount: solana.TokenProgramID,
UITokenAmount: UITokenAmount{
Amount: "2000",
Decimals: 9,
},
},
{
AccountIndex: 5,
MintAccount: accountList[22],
ProgramIDAccount: solana.TokenProgramID,
UITokenAmount: UITokenAmount{
Amount: "300",
Decimals: 9,
},
},
{
AccountIndex: 6,
MintAccount: accountList[21],
ProgramIDAccount: solana.TokenProgramID,
UITokenAmount: UITokenAmount{
Amount: "400",
Decimals: 6,
},
},
},
},
Transaction: Transaction{
Message: Message{
Instructions: []Instruction{outerInstruction},
},
},
}
tx := &Tx{rawTx: rawTx}
swaps, nextOffset, err := raydiumv4SwapV2Parser(tx, swapInstruction, innerInstructions, [2]uint{0, 1})
if err != nil {
t.Fatalf("raydiumv4SwapV2Parser() error = %v", err)
}
if len(swaps) != 1 {
t.Fatalf("raydiumv4SwapV2Parser() swaps len = %d, want 1", len(swaps))
}
if nextOffset != [2]uint{0, 4} {
t.Fatalf("raydiumv4SwapV2Parser() nextOffset = %v, want [0 4]", nextOffset)
}
swap := swaps[0]
if swap.Event != "buy" {
t.Fatalf("swap.Event = %q, want %q", swap.Event, "buy")
}
if !swap.Pool.Equals(accountList[1]) {
t.Fatalf("swap.Pool = %s, want %s", swap.Pool, accountList[1])
}
if !swap.User.Equals(accountList[7]) {
t.Fatalf("swap.User = %s, want %s", swap.User, accountList[7])
}
if !swap.EntryContract.Equals(accountList[20]) {
t.Fatalf("swap.EntryContract = %s, want %s", swap.EntryContract, accountList[20])
}
if !swap.BaseAmount.Equal(decimal.NewFromInt(42)) {
t.Fatalf("swap.BaseAmount = %s, want 42", swap.BaseAmount)
}
if !swap.QuoteAmount.Equal(decimal.NewFromInt(55)) {
t.Fatalf("swap.QuoteAmount = %s, want 55", swap.QuoteAmount)
}
}

230
swap_amounts.go Normal file
View File

@@ -0,0 +1,230 @@
package pump_parser
import (
"encoding/json"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
var maxSlippageBps = decimal.NewFromInt(10000)
func normalizeSlippageBps(value decimal.Decimal) decimal.Decimal {
//if value.IsNegative() {
// return decimal.Zero
//}
//if value.GreaterThan(maxSlippageBps) {
// return maxSlippageBps
//}
return value
}
type SwapMode uint8
type SwapAmountSide uint8
type SwapLimitType uint8
const (
SwapModeUnknown SwapMode = iota
SwapModeExactIn
SwapModeExactOut
)
const (
SwapAmountSideUnknown SwapAmountSide = iota
SwapAmountSideBase
SwapAmountSideQuote
)
const (
SwapLimitTypeUnknown SwapLimitType = iota
SwapLimitTypeMinOut
SwapLimitTypeMaxIn
)
func (m SwapMode) String() string {
switch m {
case SwapModeExactIn:
return "exact_in"
case SwapModeExactOut:
return "exact_out"
default:
return ""
}
}
func (m SwapMode) MarshalJSON() ([]byte, error) {
return json.Marshal(m.String())
}
func (s SwapAmountSide) String() string {
switch s {
case SwapAmountSideBase:
return "base"
case SwapAmountSideQuote:
return "quote"
default:
return ""
}
}
func (s SwapAmountSide) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
func (t SwapLimitType) String() string {
switch t {
case SwapLimitTypeMinOut:
return "min_out"
case SwapLimitTypeMaxIn:
return "max_in"
default:
return ""
}
}
func (t SwapLimitType) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
func swapAmountForSide(baseAmount, quoteAmount decimal.Decimal, side SwapAmountSide) decimal.Decimal {
switch side {
case SwapAmountSideBase:
return baseAmount
case SwapAmountSideQuote:
return quoteAmount
default:
return decimal.Zero
}
}
func swapMintForSide(baseMint, quoteMint solana.PublicKey, side SwapAmountSide) solana.PublicKey {
switch side {
case SwapAmountSideBase:
return baseMint
case SwapAmountSideQuote:
return quoteMint
default:
return solana.PublicKey{}
}
}
func oppositeSwapAmountSide(side SwapAmountSide) SwapAmountSide {
switch side {
case SwapAmountSideBase:
return SwapAmountSideQuote
case SwapAmountSideQuote:
return SwapAmountSideBase
default:
return SwapAmountSideUnknown
}
}
func fixedSwapAmountSide(event string, swapMode SwapMode) SwapAmountSide {
switch swapMode {
case SwapModeExactIn:
switch event {
case TxEventBuy:
return SwapAmountSideQuote
case TxEventSell:
return SwapAmountSideBase
}
case SwapModeExactOut:
switch event {
case TxEventBuy:
return SwapAmountSideBase
case TxEventSell:
return SwapAmountSideQuote
}
}
return SwapAmountSideUnknown
}
func limitSwapAmountType(swapMode SwapMode) SwapLimitType {
switch swapMode {
case SwapModeExactIn:
return SwapLimitTypeMinOut
case SwapModeExactOut:
return SwapLimitTypeMaxIn
default:
return SwapLimitTypeUnknown
}
}
func calculateLimitSlippageBps(limitType SwapLimitType, limitAmount, actualAmount decimal.Decimal) decimal.Decimal {
var value decimal.Decimal
switch limitType {
case SwapLimitTypeMinOut:
if !actualAmount.IsPositive() {
if !limitAmount.IsPositive() {
value = maxSlippageBps
break
}
value = maxSlippageBps.Neg()
break
}
if !limitAmount.IsPositive() {
value = maxSlippageBps
break
}
value = actualAmount.Sub(limitAmount).Mul(maxSlippageBps).Div(actualAmount)
case SwapLimitTypeMaxIn:
if !limitAmount.IsPositive() {
if !actualAmount.IsPositive() {
value = maxSlippageBps
break
}
value = maxSlippageBps.Neg()
break
}
value = limitAmount.Sub(actualAmount).Mul(maxSlippageBps).Div(limitAmount)
default:
value = decimal.Zero
}
return normalizeSlippageBps(value)
}
func (s *Swap) SetSwapAmountInfoDetailed(
swapMode SwapMode,
fixedAmount decimal.Decimal,
fixedSide SwapAmountSide,
fixedMint solana.PublicKey,
limitType SwapLimitType,
limitAmount decimal.Decimal,
limitSide SwapAmountSide,
limitMint solana.PublicKey,
actualLimitAmount decimal.Decimal,
) {
s.SwapMode = swapMode
s.FixedAmount = fixedAmount
s.FixedAmountSide = fixedSide
s.FixedMint = fixedMint
s.LimitAmountType = limitType
s.LimitAmount = limitAmount
s.LimitAmountSide = limitSide
s.LimitMint = limitMint
s.ActualLimitAmount = actualLimitAmount
s.ActualLimitAmountSide = limitSide
s.SlippageBps = calculateLimitSlippageBps(limitType, limitAmount, actualLimitAmount)
}
func (s *Swap) SetSwapAmountInfo(swapMode SwapMode, fixedAmount, limitAmount decimal.Decimal) {
fixedSide := fixedSwapAmountSide(s.Event, swapMode)
if fixedSide == SwapAmountSideUnknown {
return
}
limitType := limitSwapAmountType(swapMode)
limitSide := oppositeSwapAmountSide(fixedSide)
actualLimitAmount := swapAmountForSide(s.BaseAmount, s.QuoteAmount, limitSide)
s.SetSwapAmountInfoDetailed(
swapMode,
fixedAmount,
fixedSide,
swapMintForSide(s.BaseMint, s.QuoteMint, fixedSide),
limitType,
limitAmount,
limitSide,
swapMintForSide(s.BaseMint, s.QuoteMint, limitSide),
actualLimitAmount,
)
}

387
swap_amounts_oracle_test.go Normal file
View File

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

206
swap_amounts_test.go Normal file
View File

@@ -0,0 +1,206 @@
package pump_parser
import (
"testing"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
func TestSetSwapAmountInfoExactInBuy(t *testing.T) {
swap := Swap{
Event: TxEventBuy,
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
BaseAmount: decimal.NewFromInt(120),
QuoteAmount: decimal.NewFromInt(100),
}
swap.SetSwapAmountInfo(SwapModeExactIn, decimal.NewFromInt(100), decimal.NewFromInt(110))
if swap.FixedAmountSide != SwapAmountSideQuote {
t.Fatalf("fixed side = %s, want quote", swap.FixedAmountSide.String())
}
if swap.LimitAmountType != SwapLimitTypeMinOut {
t.Fatalf("limit type = %s, want min_out", swap.LimitAmountType.String())
}
if swap.LimitAmountSide != SwapAmountSideBase {
t.Fatalf("limit side = %s, want base", swap.LimitAmountSide.String())
}
if !swap.ActualLimitAmount.Equal(decimal.NewFromInt(120)) {
t.Fatalf("actual limit amount = %s, want 120", swap.ActualLimitAmount)
}
if got := swap.SlippageBps.StringFixed(4); got != "833.3333" {
t.Fatalf("slippage bps = %s, want 833.3333", got)
}
}
func TestSetSwapAmountInfoExactOutSell(t *testing.T) {
swap := Swap{
Event: TxEventSell,
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
BaseAmount: decimal.NewFromInt(95),
QuoteAmount: decimal.NewFromInt(100),
}
swap.SetSwapAmountInfo(SwapModeExactOut, decimal.NewFromInt(100), decimal.NewFromInt(105))
if swap.FixedAmountSide != SwapAmountSideQuote {
t.Fatalf("fixed side = %s, want quote", swap.FixedAmountSide.String())
}
if swap.LimitAmountType != SwapLimitTypeMaxIn {
t.Fatalf("limit type = %s, want max_in", swap.LimitAmountType.String())
}
if swap.LimitAmountSide != SwapAmountSideBase {
t.Fatalf("limit side = %s, want base", swap.LimitAmountSide.String())
}
if !swap.ActualLimitAmount.Equal(decimal.NewFromInt(95)) {
t.Fatalf("actual limit amount = %s, want 95", swap.ActualLimitAmount)
}
if got := swap.SlippageBps.StringFixed(4); got != "952.3810" {
t.Fatalf("slippage bps = %s, want 952.3810", got)
}
}
func TestSetSwapAmountInfoExactInZeroLimitUsesMaxSlippage(t *testing.T) {
swap := Swap{
Event: TxEventSell,
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
BaseAmount: decimal.NewFromInt(50),
QuoteAmount: decimal.NewFromInt(25),
}
swap.SetSwapAmountInfo(SwapModeExactIn, decimal.NewFromInt(50), decimal.Zero)
if got := swap.SlippageBps.String(); got != "10000" {
t.Fatalf("slippage bps = %s, want 10000", got)
}
}
func TestSetSwapAmountInfoExactInNegativeHeadroomClampsToZero(t *testing.T) {
swap := Swap{
Event: TxEventBuy,
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
BaseAmount: decimal.NewFromInt(90),
QuoteAmount: decimal.NewFromInt(100),
}
swap.SetSwapAmountInfo(SwapModeExactIn, decimal.NewFromInt(100), decimal.NewFromInt(110))
if got := swap.SlippageBps.String(); got != "0" {
t.Fatalf("slippage bps = %s, want 0", got)
}
}
func TestSetSwapAmountInfoExactOutNegativeHeadroomClampsToZero(t *testing.T) {
swap := Swap{
Event: TxEventSell,
BaseMint: solana.MustPublicKeyFromBase58("11111111111111111111111111111111"),
QuoteMint: solana.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112"),
BaseAmount: decimal.NewFromInt(120),
QuoteAmount: decimal.NewFromInt(100),
}
swap.SetSwapAmountInfo(SwapModeExactOut, decimal.NewFromInt(100), decimal.NewFromInt(105))
if got := swap.SlippageBps.String(); got != "0" {
t.Fatalf("slippage bps = %s, want 0", got)
}
}
func TestMeteoraDammSwapAmountInfo(t *testing.T) {
tests := []struct {
name string
event string
params *struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}
wantMode SwapMode
wantFixed int64
wantLimit int64
}{
{
name: "sell exact in uses amount0 as input and amount1 as min out",
event: TxEventSell,
params: &struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}{Amount0: 100, Amount1: 95, SwapMode: 0},
wantMode: SwapModeExactIn,
wantFixed: 100,
wantLimit: 95,
},
{
name: "sell partial fill follows exact in semantics",
event: TxEventSell,
params: &struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}{Amount0: 101, Amount1: 96, SwapMode: 1},
wantMode: SwapModeExactIn,
wantFixed: 101,
wantLimit: 96,
},
{
name: "buy exact in keeps amount0 as input and amount1 as min out",
event: TxEventBuy,
params: &struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}{Amount0: 130, Amount1: 120, SwapMode: 0},
wantMode: SwapModeExactIn,
wantFixed: 130,
wantLimit: 120,
},
{
name: "buy exact out uses amount0 as target output and amount1 as max input",
event: TxEventBuy,
params: &struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}{Amount0: 120, Amount1: 130, SwapMode: 2},
wantMode: SwapModeExactOut,
wantFixed: 120,
wantLimit: 130,
},
{
name: "sell exact out keeps amount0 as target output and amount1 as max input",
event: TxEventSell,
params: &struct {
Amount0 uint64
Amount1 uint64
SwapMode uint8
}{Amount0: 140, Amount1: 150, SwapMode: 2},
wantMode: SwapModeExactOut,
wantFixed: 140,
wantLimit: 150,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotMode, gotFixed, gotLimit, ok := meteoraDammSwapAmountInfo(tt.event, tt.params)
if !ok {
t.Fatal("ok = false, want true")
}
if gotMode != tt.wantMode {
t.Fatalf("mode = %s, want %s", gotMode.String(), tt.wantMode.String())
}
if !gotFixed.Equal(decimal.NewFromInt(tt.wantFixed)) {
t.Fatalf("fixed = %s, want %d", gotFixed, tt.wantFixed)
}
if !gotLimit.Equal(decimal.NewFromInt(tt.wantLimit)) {
t.Fatalf("limit = %s, want %d", gotLimit, tt.wantLimit)
}
})
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

28
tx.go
View File

@@ -31,6 +31,18 @@ type Swap struct {
BaseAmount decimal.Decimal
QuoteAmount decimal.Decimal
SwapMode SwapMode
FixedAmount decimal.Decimal
FixedAmountSide SwapAmountSide
FixedMint solana.PublicKey
LimitAmountType SwapLimitType
LimitAmount decimal.Decimal
LimitAmountSide SwapAmountSide
LimitMint solana.PublicKey
ActualLimitAmount decimal.Decimal
ActualLimitAmountSide SwapAmountSide
SlippageBps decimal.Decimal
BaseReserve decimal.Decimal
QuoteReserve decimal.Decimal
Mayhem bool
@@ -52,18 +64,18 @@ type Swap struct {
StartBinId int32
EndBinId int32
RemoveBp int32
BinChanges []DlmmBinLiquidityChange
PositionAccount solana.PublicKey
FeeAmount decimal.Decimal
FeeBps string
LpFeeAmount decimal.Decimal
FeeSide string
FeeMint solana.PublicKey
FeeTokenProgram solana.PublicKey
FeeMintDecimals uint8
ConsumeUnit uint64
}
type DlmmBinLiquidityChange struct {
BinId int32
AmountX decimal.Decimal
AmountY decimal.Decimal
BpsToRemove uint16
}
type platformInfo struct {
Platform string
PlatformFee decimal.Decimal

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

847
tx_binary_test.go Normal file
View File

@@ -0,0 +1,847 @@
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 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)
}
})
}
}
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)
}
}
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 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)
}
}
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
}
func testBatchHeader(skip bool) []byte {
header := []byte("BHDR\x00")
if skip {
header[4] = 1
}
return header
}