Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51f1511c8f | ||
|
|
7dfe003e5b | ||
|
|
fe94888b14 | ||
|
|
1dd843c393 | ||
|
|
d2879efcc6 | ||
|
|
e761fd6f84 | ||
|
|
ab0e87a48a | ||
|
|
fb8d93f426 | ||
|
|
0cc843b370 | ||
|
|
d9a214b4b4 | ||
|
|
047b549d0f | ||
|
|
9327eab010 |
156
SLIPPAGE_MAPPING.md
Normal file
156
SLIPPAGE_MAPPING.md
Normal 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`
|
||||
134
cmd/measure_tx_binary_block/main.go
Normal file
134
cmd/measure_tx_binary_block/main.go
Normal 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
133
cmd/rpc_parse/main.go
Normal 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()
|
||||
}
|
||||
}
|
||||
15
enum.go
15
enum.go
@@ -123,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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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 {
|
||||
|
||||
11
meta.go
11
meta.go
@@ -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")
|
||||
@@ -89,9 +93,14 @@ var (
|
||||
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")
|
||||
|
||||
645
metaoradlmm.go
645
metaoradlmm.go
@@ -66,6 +66,13 @@ type dlmmPositionCloseEvent struct {
|
||||
Owner solana.PublicKey
|
||||
}
|
||||
|
||||
type dlmmLbPairCreateEvent struct {
|
||||
LbPair solana.PublicKey
|
||||
BinStep uint16
|
||||
TokenX solana.PublicKey
|
||||
TokenY solana.PublicKey
|
||||
}
|
||||
|
||||
type dlmmClaimFeeInnerEvent struct {
|
||||
LbPair solana.PublicKey
|
||||
Position solana.PublicKey
|
||||
@@ -178,6 +185,53 @@ type dlmmAddLiquidityByWeightArgs struct {
|
||||
LiquidityParameter dlmmLiquidityParameterByWeight
|
||||
}
|
||||
|
||||
type dlmmLiquidityOneSideParameter struct {
|
||||
Amount uint64
|
||||
ActiveID int32
|
||||
MaxActiveBinSlippage int32
|
||||
BinLiquidityDist []dlmmBinLiquidityDistributionByWeight
|
||||
}
|
||||
|
||||
type dlmmLiquidityParameterByStrategyOneSide struct {
|
||||
Amount uint64
|
||||
ActiveID int32
|
||||
MaxActiveBinSlippage int32
|
||||
StrategyParameters dlmmStrategyParameters
|
||||
}
|
||||
|
||||
type dlmmAddLiquidityOneSideArgs struct {
|
||||
LiquidityParameter dlmmLiquidityOneSideParameter
|
||||
}
|
||||
|
||||
type dlmmAddLiquidityByStrategyOneSideArgs struct {
|
||||
LiquidityParameter dlmmLiquidityParameterByStrategyOneSide
|
||||
}
|
||||
|
||||
type dlmmCompressedBinDepositAmount struct {
|
||||
BinID int32
|
||||
Amount uint32
|
||||
}
|
||||
|
||||
type dlmmAddLiquiditySingleSidePreciseParameter struct {
|
||||
Bins []dlmmCompressedBinDepositAmount
|
||||
DecompressMultiplier uint64
|
||||
}
|
||||
|
||||
type dlmmAddLiquiditySingleSidePreciseParameter2 struct {
|
||||
Bins []dlmmCompressedBinDepositAmount
|
||||
DecompressMultiplier uint64
|
||||
MaxAmount uint64
|
||||
}
|
||||
|
||||
type dlmmAddLiquidityOneSidePreciseArgs struct {
|
||||
Parameter dlmmAddLiquiditySingleSidePreciseParameter
|
||||
}
|
||||
|
||||
type dlmmAddLiquidityOneSidePrecise2Args struct {
|
||||
LiquidityParameter dlmmAddLiquiditySingleSidePreciseParameter2
|
||||
RemainingAccountsInfo dlmmRemainingAccountsInfo
|
||||
}
|
||||
|
||||
type dlmmRemoveLiquidityArgs struct {
|
||||
BinLiquidityRemoval []dlmmBinLiquidityReduction
|
||||
}
|
||||
@@ -264,6 +318,16 @@ type dlmmLiquidityAccounts struct {
|
||||
tokenYProgramIdx int
|
||||
}
|
||||
|
||||
type dlmmOneSideLiquidityAccounts struct {
|
||||
positionIdx int
|
||||
poolIdx int
|
||||
userTokenIdx int
|
||||
reserveIdx int
|
||||
tokenMintIdx int
|
||||
userIdx int
|
||||
tokenProgramIdx int
|
||||
}
|
||||
|
||||
var meteoraDlmmEventAuthority = func() solana.PublicKey {
|
||||
key, _, err := solana.FindProgramAddress([][]byte{[]byte("__event_authority")}, meteoraDlmmProgram)
|
||||
if err != nil {
|
||||
@@ -283,7 +347,11 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI
|
||||
|
||||
discriminator := *(*[8]byte)(decode[:8])
|
||||
switch discriminator {
|
||||
case meteoraInitializeLbPairDiscriminator:
|
||||
case meteoraInitializeCustomizablePermissionlessLbPairDiscriminator,
|
||||
meteoraInitializeCustomizablePermissionlessLbPair2Discriminator,
|
||||
meteoraInitializeLbPairDiscriminator,
|
||||
meteoraInitializeLbPair2Discriminator,
|
||||
meteoraInitializePermissionLbPairDiscriminator:
|
||||
return metaoradlmmInitializeParser(tx, instruction, innerInstructions, offset)
|
||||
case meteoraDlmmInitializePositionDiscriminator, meteoraDlmmInitializePosition2Discriminator,
|
||||
meteoraDlmmInitializePositionByOperatorDiscriminator, meteoraDlmmInitializePositionPdaDiscriminator:
|
||||
@@ -294,13 +362,15 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI
|
||||
return metaoradlmmSwap2Parser(tx, instruction, innerInstructions, offset)
|
||||
case meteoraDlmmAddLiquidityDiscriminator, meteoraDlmmAddLiquidity2Discriminator,
|
||||
meteoraDlmmAddLiquidityByStrategyDiscriminator, meteoraDlmmAddLiquidityByStrategy2Discriminator,
|
||||
meteoraDlmmAddLiquidityByWeightDiscriminator:
|
||||
meteoraDlmmAddLiquidityByWeightDiscriminator, meteoraDlmmAddLiquidityOneSideDiscriminator,
|
||||
meteoraDlmmAddLiquidityOneSidePreciseDiscriminator, meteoraDlmmAddLiquidityOneSidePrecise2Discriminator,
|
||||
meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator:
|
||||
return metaoradlmmAddLiquidityParser(tx, instruction, innerInstructions, offset)
|
||||
case meteoraDlmmClaimFeeDiscriminator, meteoraDlmmClaimFee2Discriminator:
|
||||
return metaoradlmmClaimFeeParser(tx, instruction, innerInstructions, offset)
|
||||
case meteoraDlmmRebalanceLiquidityDiscriminator:
|
||||
return metaoradlmmRebalanceLiquidityParser(tx, instruction, innerInstructions, offset)
|
||||
case meteoraDlmmRemoveLiquidityDiscriminator, meteoraDlmmRemoveLiquidity2Discriminator,
|
||||
case meteoraDlmmRemoveAllLiquidityDiscriminator, meteoraDlmmRemoveLiquidityDiscriminator, meteoraDlmmRemoveLiquidity2Discriminator,
|
||||
meteoraDlmmRemoveLiquidityByRangeDiscriminator, meteoraDlmmRemoveLiquidityByRange2Discriminator:
|
||||
return metaoradlmmRemoveLiquidityParser(tx, instruction, innerInstructions, offset)
|
||||
case meteoraDlmmClosePositionDiscriminator, meteoraDlmmClosePosition2Discriminator, meteoraDlmmClosePositionIfEmptyDiscriminator:
|
||||
@@ -310,53 +380,131 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI
|
||||
}
|
||||
}
|
||||
|
||||
type dlmmInitializeAccounts struct {
|
||||
pool solana.PublicKey
|
||||
token0 solana.PublicKey
|
||||
token1 solana.PublicKey
|
||||
baseTokenProgram solana.PublicKey
|
||||
quoteTokenProgram solana.PublicKey
|
||||
user solana.PublicKey
|
||||
}
|
||||
|
||||
func resolveDlmmInitializeAccounts(result *RawTx, data []byte, accounts []int) (dlmmInitializeAccounts, error) {
|
||||
if len(data) < 8 {
|
||||
return dlmmInitializeAccounts{}, fmt.Errorf("instruction data too short")
|
||||
}
|
||||
|
||||
accountList := result.getAccountList()
|
||||
resolveAt := func(position int) (solana.PublicKey, error) {
|
||||
if position < 0 || position >= len(accounts) {
|
||||
return solana.PublicKey{}, fmt.Errorf("accounts too short, missing position %d", position)
|
||||
}
|
||||
accountIndex := accounts[position]
|
||||
if accountIndex < 0 || accountIndex >= len(accountList) {
|
||||
return solana.PublicKey{}, fmt.Errorf("account index out of range at position %d", position)
|
||||
}
|
||||
return accountList[accountIndex], nil
|
||||
}
|
||||
|
||||
resolveCommon := func(poolPos, token0Pos, token1Pos, userPos, baseTokenProgramPos, quoteTokenProgramPos int) (dlmmInitializeAccounts, error) {
|
||||
pool, err := resolveAt(poolPos)
|
||||
if err != nil {
|
||||
return dlmmInitializeAccounts{}, err
|
||||
}
|
||||
token0, err := resolveAt(token0Pos)
|
||||
if err != nil {
|
||||
return dlmmInitializeAccounts{}, err
|
||||
}
|
||||
token1, err := resolveAt(token1Pos)
|
||||
if err != nil {
|
||||
return dlmmInitializeAccounts{}, err
|
||||
}
|
||||
baseTokenProgram, err := resolveAt(baseTokenProgramPos)
|
||||
if err != nil {
|
||||
return dlmmInitializeAccounts{}, err
|
||||
}
|
||||
quoteTokenProgram, err := resolveAt(quoteTokenProgramPos)
|
||||
if err != nil {
|
||||
return dlmmInitializeAccounts{}, err
|
||||
}
|
||||
user, err := resolveAt(userPos)
|
||||
if err != nil {
|
||||
return dlmmInitializeAccounts{}, err
|
||||
}
|
||||
|
||||
return dlmmInitializeAccounts{
|
||||
pool: pool,
|
||||
token0: token0,
|
||||
token1: token1,
|
||||
baseTokenProgram: baseTokenProgram,
|
||||
quoteTokenProgram: quoteTokenProgram,
|
||||
user: user,
|
||||
}, nil
|
||||
}
|
||||
|
||||
discriminator := *(*[8]byte)(data[:8])
|
||||
switch discriminator {
|
||||
case meteoraInitializeLbPairDiscriminator,
|
||||
meteoraInitializeCustomizablePermissionlessLbPairDiscriminator:
|
||||
return resolveCommon(0, 2, 3, 8, 9, 9)
|
||||
case meteoraInitializeLbPair2Discriminator,
|
||||
meteoraInitializeCustomizablePermissionlessLbPair2Discriminator:
|
||||
return resolveCommon(0, 2, 3, 8, 11, 12)
|
||||
case meteoraInitializePermissionLbPairDiscriminator:
|
||||
return resolveCommon(1, 3, 4, 8, 11, 12)
|
||||
default:
|
||||
return dlmmInitializeAccounts{}, fmt.Errorf("unsupported initialize discriminator")
|
||||
}
|
||||
}
|
||||
|
||||
func metaoradlmmInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
market := tx.rawTx.accountList[instruction.Accounts[0]]
|
||||
token0 := tx.rawTx.accountList[instruction.Accounts[2]]
|
||||
token1 := tx.rawTx.accountList[instruction.Accounts[3]]
|
||||
accounts, err := resolveDlmmInitializeAccounts(tx.rawTx, instruction.Data, instruction.Accounts)
|
||||
if err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm initialize accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
|
||||
entryContract := tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
|
||||
|
||||
var baseDecimals uint8
|
||||
var quoteDecimals uint8
|
||||
findMintDecimals := func(mint solana.PublicKey) uint8 {
|
||||
for _, acc := range tx.rawTx.Meta.PostTokenBalances {
|
||||
if acc.MintAccount.Equals(token0) {
|
||||
baseDecimals = uint8(acc.UITokenAmount.Decimals)
|
||||
}
|
||||
if acc.MintAccount.Equals(token1) {
|
||||
quoteDecimals = uint8(acc.UITokenAmount.Decimals)
|
||||
if acc.MintAccount.Equals(mint) {
|
||||
return uint8(acc.UITokenAmount.Decimals)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
swap := Swap{
|
||||
Program: SolProgramMeteoraDLMM,
|
||||
Event: "create",
|
||||
Pool: market,
|
||||
BaseMint: token0,
|
||||
QuoteMint: token1,
|
||||
BaseTokenProgram: tx.rawTx.accountList[instruction.Accounts[11]],
|
||||
QuoteTokenProgram: tx.rawTx.accountList[instruction.Accounts[12]],
|
||||
Pool: accounts.pool,
|
||||
BaseMint: accounts.token0,
|
||||
QuoteMint: accounts.token1,
|
||||
BaseTokenProgram: accounts.baseTokenProgram,
|
||||
QuoteTokenProgram: accounts.quoteTokenProgram,
|
||||
Creator: tx.rawTx.accountList[0],
|
||||
BaseMintDecimals: baseDecimals,
|
||||
QuoteMintDecimals: quoteDecimals,
|
||||
User: tx.rawTx.accountList[instruction.Accounts[8]],
|
||||
BaseMintDecimals: findMintDecimals(accounts.token0),
|
||||
QuoteMintDecimals: findMintDecimals(accounts.token1),
|
||||
User: accounts.user,
|
||||
EntryContract: entryContract,
|
||||
}
|
||||
var prefixLen = offset[1]
|
||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||
createEvent, nextOffset, found, err := dlmmLbPairCreateEventFromInnerInstructions(innerInstructions, instruction, offset)
|
||||
if err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("pump create get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
return nil, nextOffset, err
|
||||
}
|
||||
var programIndex = instruction.ProgramIDIndex
|
||||
|
||||
for innerIndex, innerInstr := range inners {
|
||||
if innerInstr.ProgramIDIndex == programIndex && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraInitializeLbPairEventDiscriminator[:]) {
|
||||
if offset[1] == 0 {
|
||||
offset[0] += 1
|
||||
} else {
|
||||
offset[1] = uint(innerIndex) + 1 + prefixLen
|
||||
if found {
|
||||
offset = nextOffset
|
||||
if !createEvent.LbPair.IsZero() {
|
||||
swap.Pool = createEvent.LbPair
|
||||
}
|
||||
break
|
||||
if !createEvent.TokenX.IsZero() {
|
||||
swap.BaseMint = createEvent.TokenX
|
||||
}
|
||||
if !createEvent.TokenY.IsZero() {
|
||||
swap.QuoteMint = createEvent.TokenY
|
||||
}
|
||||
swap.BaseMintDecimals = findMintDecimals(swap.BaseMint)
|
||||
swap.QuoteMintDecimals = findMintDecimals(swap.QuoteMint)
|
||||
}
|
||||
return []Swap{swap}, offset, nil
|
||||
}
|
||||
@@ -410,10 +558,13 @@ func metaoradlmmPositionCreateParser(tx *Tx, instruction Instruction, innerInstr
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm create position accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
|
||||
createEvent, nextOffset, err := dlmmPositionCreateEventFromInnerInstructions(innerInstructions, instruction, offset)
|
||||
createEvent, nextOffset, found, err := dlmmPositionCreateEventFromInnerInstructions(innerInstructions, instruction, offset)
|
||||
if err != nil {
|
||||
return nil, nextOffset, err
|
||||
}
|
||||
if !found {
|
||||
return nil, nextOffset, InstructionIgnoredError
|
||||
}
|
||||
offset = nextOffset
|
||||
|
||||
if !createEvent.LbPair.IsZero() {
|
||||
@@ -515,22 +666,33 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
||||
}
|
||||
|
||||
discriminator := *(*[8]byte)(decode[:8])
|
||||
var swapMode SwapMode
|
||||
var fixedAmount decimal.Decimal
|
||||
var limitAmount decimal.Decimal
|
||||
switch discriminator {
|
||||
case meteoraDlmmSwapDiscriminator, meteoraDlmmSwap2Discriminator:
|
||||
var args meteoraDlmmSwapArgs
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
swapMode = SwapModeExactIn
|
||||
fixedAmount = decimal.NewFromUint64(args.AmountIn)
|
||||
limitAmount = decimal.NewFromUint64(args.MinAmountOut)
|
||||
case meteoraDlmmSwapExactOutDiscriminator, meteoraDlmmSwapExactOut2Discriminator:
|
||||
var args meteoraDlmmSwapExactOutArgs
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap_exact_out decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
swapMode = SwapModeExactOut
|
||||
fixedAmount = decimal.NewFromUint64(args.OutAmount)
|
||||
limitAmount = decimal.NewFromUint64(args.MaxInAmount)
|
||||
case meteoraDlmmSwapWithPriceImpactDiscriminator, meteoraDlmmSwapWithPriceImpact2Discriminator:
|
||||
var args meteoraDlmmSwapWithPriceImpactArgs
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm swap_with_price_impact decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
swapMode = SwapModeExactIn
|
||||
fixedAmount = decimal.NewFromUint64(args.AmountIn)
|
||||
default:
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
@@ -667,6 +829,18 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
||||
userQuote = userQuote.Add(decimal.NewFromUint64(solAmount))
|
||||
}
|
||||
}
|
||||
feeAmount, feeSide, feeMint, feeTokenProgram, feeDecimals := dlmmSwapFeeInfo(
|
||||
baseIsX,
|
||||
swapForY,
|
||||
swapEvent.Fee,
|
||||
baseMint,
|
||||
quoteMint,
|
||||
baseTokenProgram,
|
||||
quoteTokenProgram,
|
||||
baseDecimals,
|
||||
quoteDecimals,
|
||||
)
|
||||
lpFeeAmount := dlmmSwapLpFeeAmount(swapEvent.Fee, swapEvent.ProtocolFee, swapEvent.HostFee)
|
||||
|
||||
swap := Swap{
|
||||
Program: SolProgramMeteoraDLMM,
|
||||
@@ -682,6 +856,13 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
||||
User: eventUser,
|
||||
BaseAmount: baseAmount,
|
||||
QuoteAmount: quoteAmount,
|
||||
FeeAmount: feeAmount,
|
||||
FeeBps: dlmmSwapFeeBpsString(swapEvent.FeeBps),
|
||||
LpFeeAmount: lpFeeAmount,
|
||||
FeeSide: feeSide,
|
||||
FeeMint: feeMint,
|
||||
FeeTokenProgram: feeTokenProgram,
|
||||
FeeMintDecimals: feeDecimals,
|
||||
BaseReserve: baseReserve,
|
||||
QuoteReserve: quoteReserve,
|
||||
UserBaseBalance: userBase,
|
||||
@@ -691,6 +872,7 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
|
||||
StartBinId: swapEvent.StartBinId,
|
||||
EndBinId: swapEvent.EndBinId,
|
||||
}
|
||||
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
|
||||
|
||||
return []Swap{swap}, offset, nil
|
||||
}
|
||||
@@ -699,6 +881,39 @@ func metaoradlmmSwap2Parser(tx *Tx, instruction Instruction, innerInstructions I
|
||||
return metaoradlmmSwapParser(tx, instruction, innerInstructions, offset)
|
||||
}
|
||||
|
||||
func dlmmSwapFeeInfo(
|
||||
baseIsX bool,
|
||||
swapForY bool,
|
||||
fee uint64,
|
||||
baseMint solana.PublicKey,
|
||||
quoteMint solana.PublicKey,
|
||||
baseTokenProgram solana.PublicKey,
|
||||
quoteTokenProgram solana.PublicKey,
|
||||
baseDecimals uint8,
|
||||
quoteDecimals uint8,
|
||||
) (decimal.Decimal, string, solana.PublicKey, solana.PublicKey, uint8) {
|
||||
feeAmount := decimal.NewFromUint64(fee)
|
||||
if baseIsX == swapForY {
|
||||
return feeAmount, "base", baseMint, baseTokenProgram, baseDecimals
|
||||
}
|
||||
return feeAmount, "quote", quoteMint, quoteTokenProgram, quoteDecimals
|
||||
}
|
||||
|
||||
func dlmmSwapLpFeeAmount(fee, protocolFee, hostFee uint64) decimal.Decimal {
|
||||
total := decimal.NewFromUint64(fee)
|
||||
protocol := decimal.NewFromUint64(protocolFee)
|
||||
host := decimal.NewFromUint64(hostFee)
|
||||
lpFee := total.Sub(protocol).Sub(host)
|
||||
if lpFee.IsNegative() {
|
||||
return decimal.Zero
|
||||
}
|
||||
return lpFee
|
||||
}
|
||||
|
||||
func dlmmSwapFeeBpsString(feeBps agbinary.Uint128) string {
|
||||
return feeBps.DecimalString()
|
||||
}
|
||||
|
||||
func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
|
||||
result := tx.rawTx
|
||||
|
||||
@@ -726,7 +941,7 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
||||
weightDist []dlmmBinLiquidityDistributionByWeight
|
||||
startBinId int32
|
||||
endBinId int32
|
||||
hasRange bool
|
||||
oneSide bool
|
||||
)
|
||||
|
||||
switch discriminator {
|
||||
@@ -739,7 +954,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
||||
amountY = args.LiquidityParameter.AmountY
|
||||
binDist = args.LiquidityParameter.BinLiquidityDist
|
||||
startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist)
|
||||
hasRange = len(binDist) > 0
|
||||
case meteoraDlmmAddLiquidity2Discriminator:
|
||||
var args dlmmAddLiquidity2Args
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
@@ -749,7 +963,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
||||
amountY = args.LiquidityParameter.AmountY
|
||||
binDist = args.LiquidityParameter.BinLiquidityDist
|
||||
startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist)
|
||||
hasRange = len(binDist) > 0
|
||||
case meteoraDlmmAddLiquidityByStrategyDiscriminator:
|
||||
var args dlmmAddLiquidityByStrategyArgs
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
@@ -759,7 +972,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
||||
amountY = args.LiquidityParameter.AmountY
|
||||
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
|
||||
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
|
||||
hasRange = true
|
||||
case meteoraDlmmAddLiquidityByStrategy2Discriminator:
|
||||
var args dlmmAddLiquidityByStrategy2Args
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
@@ -769,7 +981,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
||||
amountY = args.LiquidityParameter.AmountY
|
||||
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
|
||||
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
|
||||
hasRange = true
|
||||
case meteoraDlmmAddLiquidityByWeightDiscriminator:
|
||||
var args dlmmAddLiquidityByWeightArgs
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
@@ -779,16 +990,40 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
||||
amountY = args.LiquidityParameter.AmountY
|
||||
weightDist = args.LiquidityParameter.BinLiquidityDist
|
||||
startBinId, endBinId = dlmmMinMaxBinIdFromWeightDistribution(weightDist)
|
||||
hasRange = len(weightDist) > 0
|
||||
case meteoraDlmmAddLiquidityOneSideDiscriminator:
|
||||
var args dlmmAddLiquidityOneSideArgs
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity one side decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
weightDist = args.LiquidityParameter.BinLiquidityDist
|
||||
startBinId, endBinId = dlmmMinMaxBinIdFromWeightDistribution(weightDist)
|
||||
oneSide = true
|
||||
case meteoraDlmmAddLiquidityOneSidePreciseDiscriminator:
|
||||
var args dlmmAddLiquidityOneSidePreciseArgs
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity one side precise decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
startBinId, endBinId = dlmmMinMaxBinIDFromCompressedDeposits(args.Parameter.Bins)
|
||||
oneSide = true
|
||||
case meteoraDlmmAddLiquidityOneSidePrecise2Discriminator:
|
||||
var args dlmmAddLiquidityOneSidePrecise2Args
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity one side precise2 decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
startBinId, endBinId = dlmmMinMaxBinIDFromCompressedDeposits(args.LiquidityParameter.Bins)
|
||||
oneSide = true
|
||||
case meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator:
|
||||
var args dlmmAddLiquidityByStrategyOneSideArgs
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity by strategy one side decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
|
||||
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
|
||||
oneSide = true
|
||||
default:
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
|
||||
accounts, err := resolveDlmmLiquidityAccounts(result, instruction.Accounts)
|
||||
if err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
|
||||
addEvent, nextOffset, err := dlmmAddLiquidityEventFromInnerInstructions(innerInstructions, instruction, offset)
|
||||
if err != nil {
|
||||
return nil, nextOffset, err
|
||||
@@ -797,14 +1032,17 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
||||
amountX = addEvent.Amounts[0]
|
||||
amountY = addEvent.Amounts[1]
|
||||
|
||||
binChanges := []DlmmBinLiquidityChange(nil)
|
||||
if len(binDist) > 0 {
|
||||
binChanges = dlmmBinChangesFromDistribution(amountX, amountY, binDist)
|
||||
} else if len(weightDist) > 0 {
|
||||
// Weight-only params do not preserve per-side amounts for each bin, so keep the affected range only.
|
||||
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, 0)
|
||||
} else if hasRange {
|
||||
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, 0)
|
||||
if oneSide {
|
||||
swaps, err := dlmmBuildOneSideAddSwap(tx, instruction, addEvent, startBinId, endBinId, entryContract)
|
||||
if err != nil {
|
||||
return nil, offset, err
|
||||
}
|
||||
return swaps, offset, nil
|
||||
}
|
||||
|
||||
accounts, err := resolveDlmmLiquidityAccounts(result, instruction.Accounts)
|
||||
if err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
|
||||
pool := result.accountList[accounts.poolIdx]
|
||||
@@ -837,7 +1075,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
||||
baseAmount = amountYDec
|
||||
quoteAmount = amountXDec
|
||||
}
|
||||
|
||||
eventUser := result.accountList[accounts.userIdx]
|
||||
if !addEvent.From.IsZero() {
|
||||
eventUser = addEvent.From
|
||||
@@ -898,7 +1135,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
|
||||
ActiveBinId: addEvent.ActiveBinId,
|
||||
StartBinId: startBinId,
|
||||
EndBinId: endBinId,
|
||||
BinChanges: binChanges,
|
||||
PositionAccount: result.accountList[accounts.positionIdx],
|
||||
}
|
||||
|
||||
@@ -926,19 +1162,19 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
||||
|
||||
discriminator := *(*[8]byte)(decode[:8])
|
||||
var (
|
||||
binChanges []DlmmBinLiquidityChange
|
||||
startBinId int32
|
||||
endBinId int32
|
||||
removeBp int32
|
||||
)
|
||||
|
||||
switch discriminator {
|
||||
case meteoraDlmmRemoveAllLiquidityDiscriminator:
|
||||
removeBp = 10000
|
||||
case meteoraDlmmRemoveLiquidityDiscriminator:
|
||||
var args dlmmRemoveLiquidityArgs
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm remove liquidity decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
binChanges = dlmmBinChangesFromReduction(args.BinLiquidityRemoval)
|
||||
startBinId, endBinId = dlmmMinMaxBinIdFromReduction(args.BinLiquidityRemoval)
|
||||
removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval)
|
||||
case meteoraDlmmRemoveLiquidity2Discriminator:
|
||||
@@ -946,7 +1182,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm remove liquidity2 decode error: %v, offset, %d, %d", err, offset[0], offset[1])
|
||||
}
|
||||
binChanges = dlmmBinChangesFromReduction(args.BinLiquidityRemoval)
|
||||
startBinId, endBinId = dlmmMinMaxBinIdFromReduction(args.BinLiquidityRemoval)
|
||||
removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval)
|
||||
case meteoraDlmmRemoveLiquidityByRangeDiscriminator:
|
||||
@@ -957,7 +1192,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
||||
startBinId = args.FromBinId
|
||||
endBinId = args.ToBinId
|
||||
removeBp = int32(args.BpsToRemove)
|
||||
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, args.BpsToRemove)
|
||||
case meteoraDlmmRemoveLiquidityByRange2Discriminator:
|
||||
var args dlmmRemoveLiquidityByRange2Args
|
||||
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
|
||||
@@ -966,7 +1200,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
||||
startBinId = args.FromBinId
|
||||
endBinId = args.ToBinId
|
||||
removeBp = int32(args.BpsToRemove)
|
||||
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, args.BpsToRemove)
|
||||
default:
|
||||
return nil, increaseOffset(offset), InstructionIgnoredError
|
||||
}
|
||||
@@ -1012,7 +1245,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
||||
baseAmount = amountYDec
|
||||
quoteAmount = amountXDec
|
||||
}
|
||||
|
||||
eventUser := result.accountList[accounts.userIdx]
|
||||
if !removeEvent.From.IsZero() {
|
||||
eventUser = removeEvent.From
|
||||
@@ -1074,7 +1306,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
|
||||
StartBinId: startBinId,
|
||||
EndBinId: endBinId,
|
||||
RemoveBp: removeBp,
|
||||
BinChanges: binChanges,
|
||||
PositionAccount: result.accountList[accounts.positionIdx],
|
||||
}
|
||||
|
||||
@@ -1318,7 +1549,6 @@ func metaoradlmmRebalanceLiquidityParser(tx *Tx, instruction Instruction, innerI
|
||||
ActiveBinId: event.ActiveBinId,
|
||||
StartBinId: event.OldMinBinId,
|
||||
EndBinId: event.OldMaxBinId,
|
||||
BinChanges: dlmmBinChangesFromRange(event.OldMinBinId, event.OldMaxBinId, 0),
|
||||
PositionAccount: result.accountList[accounts.positionIdx],
|
||||
})
|
||||
}
|
||||
@@ -1344,7 +1574,6 @@ func metaoradlmmRebalanceLiquidityParser(tx *Tx, instruction Instruction, innerI
|
||||
ActiveBinId: event.ActiveBinId,
|
||||
StartBinId: event.NewMinBinId,
|
||||
EndBinId: event.NewMaxBinId,
|
||||
BinChanges: dlmmBinChangesFromRange(event.NewMinBinId, event.NewMaxBinId, 0),
|
||||
PositionAccount: result.accountList[accounts.positionIdx],
|
||||
})
|
||||
}
|
||||
@@ -1478,11 +1707,11 @@ func dlmmRebalancingEventFromInnerInstructions(innerInstructions InnerInstructio
|
||||
return dlmmRebalancingEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm rebalance liquidity event not found, offset, %d, %d", offset[0], prefixLen)
|
||||
}
|
||||
|
||||
func dlmmPositionCreateEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmPositionCreateEvent, [2]uint, error) {
|
||||
func dlmmPositionCreateEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmPositionCreateEvent, [2]uint, bool, error) {
|
||||
var prefixLen = offset[1]
|
||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||
if err != nil {
|
||||
return dlmmPositionCreateEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm create position get inner instructions error: %v, offset, %d, %d", err, offset[0], prefixLen)
|
||||
return dlmmPositionCreateEvent{}, increaseOffset(offset), false, fmt.Errorf("meteora dlmm create position get inner instructions error: %v, offset, %d, %d", err, offset[0], prefixLen)
|
||||
}
|
||||
for innerIndex, innerInstr := range inners {
|
||||
if innerInstr.ProgramIDIndex != instruction.ProgramIDIndex {
|
||||
@@ -1497,9 +1726,9 @@ func dlmmPositionCreateEventFromInnerInstructions(innerInstructions InnerInstruc
|
||||
} else {
|
||||
offset[1] = uint(innerIndex) + 1 + prefixLen
|
||||
}
|
||||
return event, offset, nil
|
||||
return event, offset, true, nil
|
||||
}
|
||||
return dlmmPositionCreateEvent{}, increaseOffset(offset), fmt.Errorf("meteora dlmm create position event not found, offset, %d, %d", offset[0], prefixLen)
|
||||
return dlmmPositionCreateEvent{}, increaseOffset(offset), false, nil
|
||||
}
|
||||
|
||||
func dlmmPositionCloseEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmPositionCloseEvent, [2]uint, bool, error) {
|
||||
@@ -1526,6 +1755,51 @@ func dlmmPositionCloseEventFromInnerInstructions(innerInstructions InnerInstruct
|
||||
return dlmmPositionCloseEvent{}, increaseOffset(offset), false, nil
|
||||
}
|
||||
|
||||
func dlmmLbPairCreateEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmLbPairCreateEvent, [2]uint, bool, error) {
|
||||
var prefixLen = offset[1]
|
||||
inners, err := getInnerInstructions(innerInstructions, prefixLen)
|
||||
if err != nil {
|
||||
return dlmmLbPairCreateEvent{}, increaseOffset(offset), false, fmt.Errorf("meteora dlmm create get inner instructions error: %v, offset, %d, %d", err, offset[0], prefixLen)
|
||||
}
|
||||
for innerIndex, innerInstr := range inners {
|
||||
if innerInstr.ProgramIDIndex != instruction.ProgramIDIndex {
|
||||
continue
|
||||
}
|
||||
event, ok := dlmmDecodeLbPairCreateEvent(innerInstr.Data)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if offset[1] == 0 {
|
||||
offset[0] += 1
|
||||
} else {
|
||||
offset[1] = uint(innerIndex) + 1 + prefixLen
|
||||
}
|
||||
return event, offset, true, nil
|
||||
}
|
||||
return dlmmLbPairCreateEvent{}, increaseOffset(offset), false, nil
|
||||
}
|
||||
|
||||
func dlmmDecodeLbPairCreateEvent(data []byte) (dlmmLbPairCreateEvent, bool) {
|
||||
switch {
|
||||
case len(data) >= 8 && bytes.Equal(data[:8], meteoraInitializeLbPairEventDiscriminator[:]):
|
||||
var event dlmmLbPairCreateEvent
|
||||
if err := agbinary.NewBorshDecoder(data[8:]).Decode(&event); err != nil {
|
||||
return dlmmLbPairCreateEvent{}, false
|
||||
}
|
||||
return event, true
|
||||
case len(data) >= 16 &&
|
||||
bytes.Equal(data[:8], eventDiscriminator[:]) &&
|
||||
bytes.Equal(data[8:16], meteoraInitializeLbPairEventDiscriminator[:]):
|
||||
var event dlmmLbPairCreateEvent
|
||||
if err := agbinary.NewBorshDecoder(data[16:]).Decode(&event); err != nil {
|
||||
return dlmmLbPairCreateEvent{}, false
|
||||
}
|
||||
return event, true
|
||||
default:
|
||||
return dlmmLbPairCreateEvent{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func dlmmDecodeAddLiquidityEvent(data []byte) (dlmmAddLiquidityEvent, bool) {
|
||||
switch {
|
||||
case len(data) >= 8 && bytes.Equal(data[:8], meteoraDlmmAddLiquidityEventDiscriminator[:]):
|
||||
@@ -1819,6 +2093,154 @@ func resolveDlmmLiquidityAccounts(result *RawTx, accounts []int) (dlmmLiquidityA
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveDlmmOneSideLiquidityAccounts(result *RawTx, accounts []int) (dlmmOneSideLiquidityAccounts, error) {
|
||||
if len(accounts) < 10 {
|
||||
return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("accounts too short, expected at least 10")
|
||||
}
|
||||
accountList := result.accountList
|
||||
|
||||
eventAuthorityPos := -1
|
||||
for i, idx := range accounts {
|
||||
if idx < 0 || idx >= len(accountList) {
|
||||
continue
|
||||
}
|
||||
if accountList[idx].Equals(meteoraDlmmEventAuthority) {
|
||||
eventAuthorityPos = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if eventAuthorityPos == -1 {
|
||||
return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("event authority not found")
|
||||
}
|
||||
if eventAuthorityPos+1 >= len(accounts) || !accountList[accounts[eventAuthorityPos+1]].Equals(meteoraDlmmProgram) {
|
||||
return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("program id not found after event authority")
|
||||
}
|
||||
|
||||
tokenProgramPos := eventAuthorityPos - 1
|
||||
userPos := eventAuthorityPos - 2
|
||||
if tokenProgramPos < 0 || userPos < 0 {
|
||||
return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("one side liquidity account positions invalid")
|
||||
}
|
||||
if len(accounts) < 6 {
|
||||
return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("accounts too short for one side liquidity parsing")
|
||||
}
|
||||
|
||||
return dlmmOneSideLiquidityAccounts{
|
||||
positionIdx: accounts[0],
|
||||
poolIdx: accounts[1],
|
||||
userTokenIdx: accounts[3],
|
||||
reserveIdx: accounts[4],
|
||||
tokenMintIdx: accounts[5],
|
||||
userIdx: accounts[userPos],
|
||||
tokenProgramIdx: accounts[tokenProgramPos],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func dlmmBuildOneSideAddSwap(
|
||||
tx *Tx,
|
||||
instruction Instruction,
|
||||
addEvent dlmmAddLiquidityEvent,
|
||||
startBinId int32,
|
||||
endBinId int32,
|
||||
entryContract solana.PublicKey,
|
||||
) ([]Swap, error) {
|
||||
result := tx.rawTx
|
||||
accounts, err := resolveDlmmOneSideLiquidityAccounts(result, instruction.Accounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
knownMint := result.accountList[accounts.tokenMintIdx]
|
||||
knownTokenProgram := result.accountList[accounts.tokenProgramIdx]
|
||||
knownDecimals, ok := dlmmTokenDecimals(result, accounts.reserveIdx)
|
||||
if !ok {
|
||||
knownDecimals, _ = dlmmTokenDecimals(result, accounts.userTokenIdx)
|
||||
}
|
||||
knownReserveBalance := getAccountBalanceAfterTx(result, accounts.reserveIdx)
|
||||
knownUserBalance := getAccountBalanceAfterTx(result, accounts.userTokenIdx)
|
||||
if knownMint.Equals(wSolMint) {
|
||||
if solAmount, err := GetSolAfterTx(result, accounts.userIdx); err == nil {
|
||||
knownUserBalance = knownUserBalance.Add(decimal.NewFromUint64(solAmount))
|
||||
}
|
||||
}
|
||||
|
||||
eventUser := result.accountList[accounts.userIdx]
|
||||
if !addEvent.From.IsZero() {
|
||||
eventUser = addEvent.From
|
||||
}
|
||||
positionAccount := result.accountList[accounts.positionIdx]
|
||||
if !addEvent.Position.IsZero() {
|
||||
positionAccount = addEvent.Position
|
||||
}
|
||||
|
||||
swap := Swap{
|
||||
Program: SolProgramMeteoraDLMM,
|
||||
Event: "add",
|
||||
Pool: result.accountList[accounts.poolIdx],
|
||||
User: eventUser,
|
||||
EntryContract: entryContract,
|
||||
ActiveBinId: addEvent.ActiveBinId,
|
||||
StartBinId: startBinId,
|
||||
EndBinId: endBinId,
|
||||
PositionAccount: positionAccount,
|
||||
}
|
||||
|
||||
knownIsX := dlmmInferOneSideLiquidityAxis(result, accounts, addEvent)
|
||||
if knownIsX {
|
||||
swap.BaseMint = knownMint
|
||||
swap.BaseTokenProgram = knownTokenProgram
|
||||
swap.BaseMintDecimals = knownDecimals
|
||||
swap.BaseAmount = decimal.NewFromUint64(addEvent.Amounts[0])
|
||||
swap.BaseReserve = knownReserveBalance
|
||||
swap.UserBaseBalance = knownUserBalance
|
||||
if _, exists := tx.Token[knownMint]; !exists && !knownMint.Equals(wSolMint) {
|
||||
tx.Token[knownMint] = TokenMeta{
|
||||
Mint: knownMint,
|
||||
Decimals: knownDecimals,
|
||||
TokenProgram: knownTokenProgram,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
swap.QuoteMint = knownMint
|
||||
swap.QuoteTokenProgram = knownTokenProgram
|
||||
swap.QuoteMintDecimals = knownDecimals
|
||||
swap.QuoteAmount = decimal.NewFromUint64(addEvent.Amounts[1])
|
||||
swap.QuoteReserve = knownReserveBalance
|
||||
swap.UserQuoteBalance = knownUserBalance
|
||||
if _, exists := tx.Token[knownMint]; !exists && !knownMint.Equals(wSolMint) {
|
||||
tx.Token[knownMint] = TokenMeta{
|
||||
Mint: knownMint,
|
||||
Decimals: knownDecimals,
|
||||
TokenProgram: knownTokenProgram,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []Swap{swap}, nil
|
||||
}
|
||||
|
||||
func dlmmInferOneSideLiquidityAxis(result *RawTx, accounts dlmmOneSideLiquidityAccounts, addEvent dlmmAddLiquidityEvent) bool {
|
||||
knownAmount, ok := dlmmTokenDelta(result, accounts.reserveIdx)
|
||||
if !ok || knownAmount.IsZero() {
|
||||
knownAmount, _ = dlmmTokenDelta(result, accounts.userTokenIdx)
|
||||
}
|
||||
|
||||
amountX := decimal.NewFromUint64(addEvent.Amounts[0])
|
||||
amountY := decimal.NewFromUint64(addEvent.Amounts[1])
|
||||
switch {
|
||||
case !knownAmount.IsZero() && knownAmount.Equal(amountX) && !knownAmount.Equal(amountY):
|
||||
return true
|
||||
case !knownAmount.IsZero() && knownAmount.Equal(amountY) && !knownAmount.Equal(amountX):
|
||||
return false
|
||||
case addEvent.Amounts[0] > 0 && addEvent.Amounts[1] == 0:
|
||||
return true
|
||||
case addEvent.Amounts[1] > 0 && addEvent.Amounts[0] == 0:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func resolveDlmmClaimFeeAccounts(result *RawTx, data []byte, accounts []int) (dlmmLiquidityAccounts, error) {
|
||||
if len(data) < 8 {
|
||||
return dlmmLiquidityAccounts{}, fmt.Errorf("instruction data too short")
|
||||
@@ -1990,56 +2412,67 @@ func dlmmTokenBalanceMeta(result *RawTx, accountIndex int) (TokenBalance, bool)
|
||||
return TokenBalance{}, false
|
||||
}
|
||||
|
||||
func dlmmBinChangesFromDistribution(amountX, amountY uint64, dist []dlmmBinLiquidityDistribution) []DlmmBinLiquidityChange {
|
||||
if len(dist) == 0 {
|
||||
func dlmmAllocateByWeights(total uint64, weights []uint64) []decimal.Decimal {
|
||||
if len(weights) == 0 {
|
||||
return nil
|
||||
}
|
||||
totalX := decimal.NewFromUint64(amountX)
|
||||
totalY := decimal.NewFromUint64(amountY)
|
||||
denom := decimal.NewFromInt(10000)
|
||||
changes := make([]DlmmBinLiquidityChange, 0, len(dist))
|
||||
for _, item := range dist {
|
||||
x := totalX.Mul(decimal.NewFromInt(int64(item.DistributionX))).Div(denom).Truncate(0)
|
||||
y := totalY.Mul(decimal.NewFromInt(int64(item.DistributionY))).Div(denom).Truncate(0)
|
||||
changes = append(changes, DlmmBinLiquidityChange{
|
||||
BinId: item.BinId,
|
||||
AmountX: x,
|
||||
AmountY: y,
|
||||
})
|
||||
|
||||
sumWeights := uint64(0)
|
||||
for _, weight := range weights {
|
||||
sumWeights += weight
|
||||
}
|
||||
return changes
|
||||
if sumWeights == 0 {
|
||||
sumWeights = uint64(len(weights))
|
||||
weights = append([]uint64(nil), weights...)
|
||||
for i := range weights {
|
||||
weights[i] = 1
|
||||
}
|
||||
}
|
||||
|
||||
allocations := make([]decimal.Decimal, len(weights))
|
||||
remaining := total
|
||||
for i, weight := range weights {
|
||||
amount := uint64(0)
|
||||
if i == len(weights)-1 {
|
||||
amount = remaining
|
||||
} else if sumWeights > 0 {
|
||||
amount = total * weight / sumWeights
|
||||
if amount > remaining {
|
||||
amount = remaining
|
||||
}
|
||||
remaining -= amount
|
||||
}
|
||||
allocations[i] = decimal.NewFromUint64(amount)
|
||||
}
|
||||
return allocations
|
||||
}
|
||||
|
||||
func dlmmBinChangesFromReduction(reduction []dlmmBinLiquidityReduction) []DlmmBinLiquidityChange {
|
||||
if len(reduction) == 0 {
|
||||
return nil
|
||||
func dlmmApplySignedAllocation(values []decimal.Decimal, negative bool) []decimal.Decimal {
|
||||
if !negative {
|
||||
return values
|
||||
}
|
||||
changes := make([]DlmmBinLiquidityChange, 0, len(reduction))
|
||||
for _, item := range reduction {
|
||||
changes = append(changes, DlmmBinLiquidityChange{
|
||||
BinId: item.BinId,
|
||||
BpsToRemove: item.BpsToRemove,
|
||||
})
|
||||
out := make([]decimal.Decimal, len(values))
|
||||
for i, value := range values {
|
||||
out[i] = value.Neg()
|
||||
}
|
||||
return changes
|
||||
return out
|
||||
}
|
||||
|
||||
func dlmmBinChangesFromRange(startBinId, endBinId int32, bpsToRemove uint16) []DlmmBinLiquidityChange {
|
||||
if startBinId > endBinId {
|
||||
startBinId, endBinId = endBinId, startBinId
|
||||
func dlmmMinMaxBinIDFromCompressedDeposits(bins []dlmmCompressedBinDepositAmount) (startBinID, endBinID int32) {
|
||||
if len(bins) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
count := int(endBinId-startBinId) + 1
|
||||
if count <= 0 {
|
||||
return nil
|
||||
startBinID = bins[0].BinID
|
||||
endBinID = bins[0].BinID
|
||||
for _, bin := range bins[1:] {
|
||||
if bin.BinID < startBinID {
|
||||
startBinID = bin.BinID
|
||||
}
|
||||
changes := make([]DlmmBinLiquidityChange, 0, count)
|
||||
for binId := startBinId; binId <= endBinId; binId++ {
|
||||
changes = append(changes, DlmmBinLiquidityChange{
|
||||
BinId: binId,
|
||||
BpsToRemove: bpsToRemove,
|
||||
})
|
||||
if bin.BinID > endBinID {
|
||||
endBinID = bin.BinID
|
||||
}
|
||||
return changes
|
||||
}
|
||||
return startBinID, endBinID
|
||||
}
|
||||
|
||||
func dlmmCommonRemoveBp(reduction []dlmmBinLiquidityReduction) int32 {
|
||||
|
||||
424
metaoradlmm_test.go
Normal file
424
metaoradlmm_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
|
||||
117
orcawhirpool.go
117
orcawhirpool.go
@@ -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
36
pump.go
@@ -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,
|
||||
|
||||
39
pump_test.go
39
pump_test.go
@@ -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")
|
||||
|
||||
65
pumpamm.go
65
pumpamm.go
@@ -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) {
|
||||
|
||||
2
rawtx.go
2
rawtx.go
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
44
raydiumv4.go
44
raydiumv4.go
@@ -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
144
raydiumv4_test.go
Normal 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
230
swap_amounts.go
Normal 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
387
swap_amounts_oracle_test.go
Normal 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
206
swap_amounts_test.go
Normal 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
27
tx.go
27
tx.go
@@ -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,19 +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
2194
tx_binary.go
Normal file
File diff suppressed because it is too large
Load Diff
146
tx_binary_realdata_test.go
Normal file
146
tx_binary_realdata_test.go
Normal 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
847
tx_binary_test.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user