Compare commits

...

18 Commits

Author SHA1 Message Date
thloyi
bb858c643e fix orcawhirpool int64 overflow 2026-04-22 11:10:46 +08:00
thloyi
a620df5837 fix pump parser 2026-04-21 14:18:42 +08:00
thloyi
36da96eeaf no two hp swap slippage 2026-04-20 16:31:18 +08:00
thloyi
a765fafddd fix pump parser 2026-04-20 16:26:55 +08:00
thloyi
738e417167 fix EncodeTxBinary 2026-04-20 15:25:08 +08:00
thloyi
51f1511c8f fix EncodeTxBinary 2026-04-20 15:09:42 +08:00
thloyi
7dfe003e5b fix event enum 2026-04-20 14:16:20 +08:00
thloyi
fe94888b14 fix slippage 2026-04-20 12:31:30 +08:00
thloyi
1dd843c393 batch encode opts 2026-04-16 17:56:17 +08:00
thloyi
d2879efcc6 batch encode 2026-04-16 16:40:40 +08:00
thloyi
e761fd6f84 swap amount input 2026-04-16 14:24:14 +08:00
thloyi
ab0e87a48a fix raydium v4 swap v2 2026-04-16 11:39:15 +08:00
bijianing97
fb8d93f426 Update dlmm fee 2026-04-11 08:34:21 +08:00
bijianing97
0cc843b370 Update dlmm fee 2026-04-11 08:27:34 +08:00
bijianing97
d9a214b4b4 Add dlmm add liquidity one side function 2026-03-25 11:34:46 +08:00
thloyi
047b549d0f option ComputeUnitsConsumed 2026-03-23 20:20:21 +08:00
bijianing97
9327eab010 Fix dlmm parser 2026-03-23 15:30:43 +08:00
bijianing97
0ef57cf79a Add dlmm add_liquidity_by_weight 2026-03-20 17:06:37 +08:00
42 changed files with 6504 additions and 1225 deletions

156
SLIPPAGE_MAPPING.md Normal file
View File

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

View File

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

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

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

25
enum.go
View File

@@ -119,9 +119,24 @@ func GetConditionByProgram(program string) []string {
} }
const ( const (
TxEventAddLP = "add" TxEventAddLP = "add"
TxEventRemoveLP = "remove" TxEventRemoveLP = "remove"
TxEventBuy = "buy" TxEventBuy = "buy"
TxEventSell = "sell" TxEventSell = "sell"
TxEventBurn = "burn" TxEventBuyFailed = "buy_failed"
TxEventSellFailed = "sell_failed"
TxEventBurn = "burn"
TxEventCreate = "create"
TxEventComplete = "complete"
TxEventMigrate = "migrate"
TxEventDeposit = "deposit"
TxEventWithdraw = "withdraw"
TxEventOpen = "open"
TxEventClose = "close"
TxEventClaimFee = "claim_fee"
TxEventAddLiquidity = "add_liquidity"
TxEventAddLiquidityOneSide = "add_liquidity_one_side"
TxEventRemoveLiquidity = "remove_liquidity"
TxEventRemoveLiquidityOneSide = "remove_liquidity_one_side"
) )

View File

@@ -25,7 +25,7 @@ func main() {
// laserstream-mainnet-slc.helius-rpc.com:80 // laserstream-mainnet-slc.helius-rpc.com:80
ch := make(chan example.SubscriptionMessage, 1) 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) // var tokenTxs = make(map[string]*types.Tx)
// currentBlock := uint64(0) // currentBlock := uint64(0)
for msg := range ch { 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) { if len(ptx.Swaps) > 0 {
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(), for _, swap := range ptx.Swaps {
ptx.Swaps[0].BaseAmount.Div(decimal.NewFromInt(1e6)), ptx.Swaps[0].QuoteAmount.Div(decimal.NewFromInt(1e9))) 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 // currentBlock = ptx.Block
// //

View File

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

View File

@@ -2,29 +2,18 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log"
"log/slog"
"strings"
"time"
"github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc" "github.com/gagliardetto/solana-go/rpc"
"github.com/jackc/pgtype"
"github.com/shopspring/decimal"
solana_parser "github.com/thloyi/pump-parser" solana_parser "github.com/thloyi/pump-parser"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
) )
var () var ()
func main() { func main() {
var slot uint64 = 403021435 var slot uint64 = 414696178
var data = NewBlockData(decimal.NewFromFloat(100.0))
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d") client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
var rewards = false var rewards = false
var version uint64 = 0 var version uint64 = 0
@@ -42,7 +31,7 @@ func main() {
} }
solana_parser.EnableAllParsers() solana_parser.EnableAllParsers()
var txs []*solana_parser.Tx var txs []solana_parser.Tx
for i, tx := range blocks.Transactions { for i, tx := range blocks.Transactions {
var blockTime uint64 var blockTime uint64
if blocks.BlockTime != nil { if blocks.BlockTime != nil {
@@ -61,766 +50,11 @@ func main() {
fmt.Println("parse tx error:", i, rawTx.TxHash(), err) fmt.Println("parse tx error:", i, rawTx.TxHash(), err)
break break
} }
txs = append(txs, parsedTx) txs = append(txs, *parsedTx)
} }
for _, result := range txs { _, err = solana_parser.EncodeTxsBinary(txs)
swapsLen := len(result.Swaps)
for i := 0; i < swapsLen; i++ {
action := result.Swaps[i]
var actions []solana_parser.Swap = make([]solana_parser.Swap, 0, 2)
actions = append(actions, action)
if i+1 < swapsLen {
nextAction := result.Swaps[i+1]
if action.Event == "buy" && nextAction.Event == "complete" &&
action.Program == solana_parser.SolProgramPump &&
nextAction.Program == solana_parser.SolProgramPump &&
action.BaseMint == nextAction.BaseMint {
actions = append(actions, nextAction)
i++
}
if action.Event == "migrate" && nextAction.Event == "create" &&
action.Program == solana_parser.SolProgramPump &&
nextAction.Program == solana_parser.SolProgramPumpAMM &&
action.BaseMint == nextAction.BaseMint {
actions = append(actions, nextAction)
i++
}
}
if err = HandleAction(context.Background(), result, actions, data); err != nil {
//h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err)
fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err)
}
}
}
fmt.Println("slot", slot, "tx count: ", len(data.Txs))
}
var (
meteoraDammV2Program = solana.MustPublicKeyFromBase58("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG")
raydiumCPmmProgramID = solana.MustPublicKeyFromBase58("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C")
)
func HandleAction(ctx context.Context, tx *solana_parser.Tx, swaps []solana_parser.Swap, data *BlockData) error {
swapLen := len(swaps)
if len(swaps) == 0 {
return nil
}
if swaps[0].BaseMint != solana_parser.WSOL && swaps[0].QuoteMint != solana_parser.WSOL && !swaps[0].QuoteMint.IsZero() {
return nil
}
if len(swaps) == 0 {
return nil
}
event := swaps[0].Event
swap := swaps[0]
action := SwapGetter{swap}
switch event {
case "buy", "sell":
data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price))
if swap.Program == solana_parser.SolProgramPump {
if swapLen == 2 && swaps[1].Event == "complete" {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
data.AppendAction(Action{
Maker: swaps[1].User.String(),
Token: swaps[1].BaseMint.String(),
Pair: swaps[1].Pool.String(),
Action: "pump-migrate",
Block: tx.Block,
BlockAt: t,
TxHash: tx.GetTxHash(),
})
}
}
return data.SetPair(action, tx.Block, "")
case "create":
pair, err := action.GetPair(tx.Block, "")
if err != nil {
return err
}
data.AppendTx(action.GetTx(tx, uint64(swap.TxIndex), data.Price))
data.Pairs[pair.Address] = *pair
case "add_liquidity", "remove_liquidity", "deposit", "withdraw", "add", "remove":
liquidityTx, err := action.GetLiquidityTx(tx, uint64(swap.TxIndex))
if liquidityTx == nil {
return err
}
data.AppendTx(*liquidityTx)
return data.SetPair(action, tx.Block, "")
}
if event != "migrate" {
return nil
}
if swap.Program == solana_parser.SolProgramPump {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
if swapLen == 2 && swaps[1].Event == "create" && swaps[1].Program == solana_parser.SolProgramPumpAMM && swaps[1].BaseMint == swap.BaseMint {
tokenMint := swap.BaseMint.String()
data.AppendAction(Action{
Maker: swap.User.String(),
Token: tokenMint,
Pair: swaps[1].Pool.String(),
Action: "on-pumpswap",
Block: tx.Block,
BlockAt: t,
TxHash: tx.GetTxHash(),
})
data.NewRaydium = append(data.NewRaydium, tokenMint)
}
} else if swap.Program == solana_parser.SolProgramRaydiumLaunchLab || swap.Program == solana_parser.SolProgramRaydiumLaunchLabBonk {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
var actionType string
if action.MigrateTopProgram == raydiumCPmmProgramID {
actionType = "on-raydium-cpmm"
} else {
actionType = "on-raydium-amm"
}
data.AppendAction(Action{
Maker: action.User.String(),
Token: action.BaseMint.String(),
Pair: action.MigrateToPool.String(),
Action: actionType,
Block: tx.Block,
BlockAt: t,
TxHash: tx.GetTxHash(),
})
} else if swap.Program == solana_parser.SolProgramMeteoraBondingCurve {
t := pgtype.Timestamptz{}
t.Set(time.Unix(tx.BlockAt, 0))
var actionType string
if swap.MigrateTopProgram == meteoraDammV2Program {
actionType = "on-meteora-amm-v2"
} else {
actionType = "on-meteora-amm-v1"
}
data.AppendAction(Action{
Maker: action.User.String(),
Token: action.BaseMint.String(),
Pair: action.MigrateToPool.String(),
Action: actionType,
Block: uint64(tx.Block),
BlockAt: t,
TxHash: tx.GetTxHash(),
})
}
return nil
}
type Pair struct {
Id string `gorm:"column:id;primaryKey;default:uuid_generate_v4()"`
Address string
Name string
Token0 string
Token1 string
LpToken string
ChainId int64
Reserve0 decimal.Decimal
Reserve1 decimal.Decimal
Block uint64
BlockAt *pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at,omitempty"`
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"created_at,omitempty"`
SortId uint64
Program string
IsCreate bool `gorm:"-"`
//TokenObj *Token `gorm:"-" json:"token_obj,omitempty"`
UpdateSlot uint64 `gorm:"-"`
InDB bool `gorm:"-"`
}
type Tx struct {
Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"`
PairAddress string `json:"pair_address"`
Maker string `json:"maker"`
Token0Address string `json:"token0_address"`
Token1Address string `json:"token1_address"`
Token0Amount decimal.Decimal `json:"token0Amount" gorm:"column:token0_amount;type:numeric"`
Token1Amount decimal.Decimal `json:"token1Amount" gorm:"column:token1_amount;type:numeric"`
PriceUsd decimal.Decimal `json:"price_usd" gorm:"column:price_usd;type:numeric"`
AmountUsd decimal.Decimal `json:"amount_usd" gorm:"column:amount_usd;type:numeric"`
Block uint64 `json:"block"`
BlockIndex uint64 `json:"index"`
Event string `json:"event"`
TxHash string `json:"tx_hash"`
TxIndex uint64 `json:"topic_index"`
Program string `json:"program"`
BlockAt pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at"`
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"`
TotalSupply string `gorm:"total_supply"`
AfterReserve0 string `gorm:"after_reserve0"`
AfterReserve1 string `gorm:"after_reserve1"`
PositionChange int64 `gorm:"position_change"`
Platform string `gorm:"column:tx_platform;type:platform;default:'none'" json:"tx_platform"`
PlatformFee decimal.Decimal `gorm:"-" json:"-"` // TODO: save to db
CUPrice decimal.Decimal `gorm:"column:tx_cu_price;type:numeric" json:"tx_cu_price"`
MevAgent string `gorm:"column:tx_mev_agent;type:mev_agent;default:'none'" json:"tx_mev_agent"`
MevAgentFee decimal.Decimal `gorm:"column:tx_mev_agent_fee;type:numeric" json:"tx_mev_agent_fee"`
AfterSOLBalance decimal.Decimal `gorm:"column:after_sol_balance;type:numeric" json:"after_sol_balance"`
EntryContract string `gorm:"column:tx_entry_contract;type:entry_contract;default:'none'" json:"tx_entry_contract"`
}
type Action struct {
Id pgtype.UUID `gorm:"column:id;primaryKey;default:uuid_generate_v4()" json:"-"`
Maker string `json:"maker"`
Token string `json:"token"`
Pair string `json:"pair"`
Action string `json:"action"`
Block uint64 `json:"block"`
BlockAt pgtype.Timestamptz `json:"block_at"`
TxHash string `json:"tx_hash"`
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"`
}
type BlockData struct {
Pairs map[string]Pair
Txs []Tx
Actions []Action
Price decimal.Decimal
NewRaydium []string
}
func NewBlockData(price decimal.Decimal) *BlockData {
return &BlockData{
Pairs: make(map[string]Pair),
Txs: make([]Tx, 0),
Actions: make([]Action, 0),
Price: price,
NewRaydium: make([]string, 0),
}
}
func (bd *BlockData) AppendTx(tx Tx) {
bd.Txs = append(bd.Txs, tx)
}
func (bd *BlockData) AppendAction(action Action) {
bd.Actions = append(bd.Actions, action)
}
func (bd *BlockData) SetPair(action SwapGetter, block uint64, _ string) error {
pair, err := action.GetPair(block, "")
if err != nil { if err != nil {
return err fmt.Println("EncodeTxsBinary err", err)
} }
bd.Pairs[pair.Address] = *pair
return nil
}
type SwapGetter struct {
solana_parser.Swap
}
const (
PositionChangeNone = int64(iota)
PositionChangeNewBuy
PositionChangeBuyMore
PositionChangeSellPart
PositionChangeSellAll
)
func (spg SwapGetter) GetLiquidityTx(tx *solana_parser.Tx, index uint64) (*Tx, error) {
if spg.BaseMint != solana.WrappedSol && spg.QuoteMint != solana.WrappedSol {
return nil, nil
}
var (
token0 string
amount0 decimal.Decimal
amount1 decimal.Decimal
pool0 decimal.Decimal
pool1 decimal.Decimal
event string
)
if spg.BaseMint == solana.WrappedSol {
amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
token0 = spg.QuoteMint.String()
pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
} else {
amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
token0 = spg.BaseMint.String()
pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
}
if spg.Event == "deposit" || spg.Event == "add" || spg.Event == "add_liquidity" || spg.Event == "add_liquidity_one_side" {
event = "add"
} else if spg.Event == "withdraw" || spg.Event == "remove" || spg.Event == "remove_liquidity" || spg.Event == "remove_liquidity_one_side" {
event = "remove"
}
if event == "" {
return nil, nil
}
mevName, mevFee := tx.CheckMevAgent()
platformName, platformFee := tx.CheckPlatform(spg.Swap)
pairString := ""
if spg.Program == solana_parser.SolProgramPump {
pairString = spg.BaseMint.String()
} else {
pairString = spg.Pool.String()
}
t := pgtype.Timestamptz{}
_ = t.Set(time.Unix(tx.BlockAt, 0))
return &Tx{
PairAddress: pairString,
Maker: spg.User.String(),
Token0Address: token0,
Token1Address: "So11111111111111111111111111111111111111112",
Token0Amount: amount0,
Token1Amount: amount1,
Block: tx.Block,
BlockIndex: tx.BlockIndex,
Event: event,
TxHash: tx.GetTxHash(),
TxIndex: index,
BlockAt: t,
Program: spg.Program,
AfterReserve0: pool0.String(),
AfterReserve1: pool1.String(),
Platform: platformName,
PlatformFee: platformFee,
CUPrice: tx.CUPrice,
MevAgent: mevName,
MevAgentFee: mevFee,
AfterSOLBalance: spg.AfterSOLBalance,
EntryContract: spg.CheckEntryContract(),
}, nil
}
func (spg SwapGetter) GetTx(tx *solana_parser.Tx, index uint64, price decimal.Decimal) Tx {
var (
token0 string
amount0 decimal.Decimal
amount1 decimal.Decimal
pool0 decimal.Decimal
pool1 decimal.Decimal
event string
)
if spg.BaseMint == solana.WrappedSol {
amount0 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
amount1 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
token0 = spg.QuoteMint.String()
pool0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
pool1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
if spg.Event == "buy" {
event = "sell"
} else if spg.Event == "sell" {
event = "buy"
}
} else {
amount0 = spg.BaseAmount.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
amount1 = spg.QuoteAmount.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
token0 = spg.BaseMint.String()
pool0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
pool1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
event = spg.Event
}
priceUsd := decimal.Zero
if amount0.GreaterThan(priceUsd) {
priceUsd = amount1.Div(amount0).Mul(price)
}
pc := PositionChangeNone
if event == "buy" {
pc = PositionChangeNewBuy
if spg.BaseMint == solana.WrappedSol {
if spg.UserQuoteBalance.GreaterThan(spg.QuoteAmount) {
pc = PositionChangeBuyMore
}
} else {
if spg.UserBaseBalance.GreaterThan(spg.BaseAmount) {
pc = PositionChangeBuyMore
}
}
} else if event == "sell" {
pc = PositionChangeSellPart
if spg.BaseMint == solana.WrappedSol {
if spg.UserQuoteBalance.Div(decimal.New(1, int32(spg.QuoteMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) {
pc = PositionChangeSellAll
}
} else {
if spg.UserBaseBalance.Div(decimal.New(1, int32(spg.BaseMintDecimals))).LessThan(decimal.NewFromFloat(0.01)) {
pc = PositionChangeSellAll
}
}
}
mevName, mevFee := tx.CheckMevAgent()
platformName, platformFee := tx.CheckPlatform(spg.Swap)
if mevName == "" {
mevName = "none"
}
if mevName == "unknown" {
mevName = "none"
mevFee = decimal.Zero
}
pairString := ""
if spg.Program == solana_parser.SolProgramPump {
pairString = spg.BaseMint.String()
} else {
pairString = spg.Pool.String()
}
t := pgtype.Timestamptz{}
_ = t.Set(time.Unix(tx.BlockAt, 0))
return Tx{
PairAddress: pairString,
Maker: spg.User.String(),
Token0Address: token0,
Token1Address: "So11111111111111111111111111111111111111112",
Token0Amount: amount0,
Token1Amount: amount1,
PriceUsd: priceUsd,
AmountUsd: amount1.Mul(price),
Block: tx.Block,
BlockIndex: tx.BlockIndex,
Event: event,
TxHash: tx.GetTxHash(),
TxIndex: index,
BlockAt: t,
Program: spg.Program,
AfterReserve0: pool0.String(),
AfterReserve1: pool1.String(),
PositionChange: pc,
Platform: platformName,
PlatformFee: platformFee,
CUPrice: tx.CUPrice,
MevAgent: mevName,
MevAgentFee: mevFee,
AfterSOLBalance: spg.AfterSOLBalance,
EntryContract: spg.CheckEntryContract(),
}
}
func (spg SwapGetter) GetPair(slot uint64, _ string) (*Pair, error) {
//pump amm
if spg.Program == solana_parser.SolProgramPump {
tokenMint := spg.BaseMint.String()
return &Pair{
Address: tokenMint,
Token0: tokenMint,
Token1: "So11111111111111111111111111111111111111112",
ChainId: 900,
Reserve0: spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals))),
Reserve1: spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals))),
IsCreate: spg.Event == "create",
Program: spg.Program,
UpdateSlot: slot,
}, nil
} else {
var (
token0 string
amount0 decimal.Decimal
amount1 decimal.Decimal
)
if spg.BaseMint.IsZero() || spg.QuoteMint.IsZero() {
return nil, errors.New("base mint or quote mint is empty")
}
if spg.BaseMint == solana.WrappedSol {
amount0 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
amount1 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
//decimal0 = spg.QuoteMintDecimals
token0 = spg.QuoteMint.String()
} else {
amount0 = spg.BaseReserve.Div(decimal.New(1, int32(spg.BaseMintDecimals)))
amount1 = spg.QuoteReserve.Div(decimal.New(1, int32(spg.QuoteMintDecimals)))
//decimal0 = a.BaseDecimals
token0 = spg.BaseMint.String()
}
return &Pair{
Address: spg.Pool.String(),
LpToken: spg.LpMint.String(),
Token0: token0,
Token1: "So11111111111111111111111111111111111111112",
ChainId: 900,
Reserve0: amount0,
Reserve1: amount1,
IsCreate: false,
Program: spg.Program,
UpdateSlot: slot,
}, nil
}
}
func getBlockTxsFromDb(db *gorm.DB, block uint64) ([]Tx, error) {
var txs []Tx
result := db.Table("tx").Where("block = ?", block).Find(&txs)
return txs, result.Error
}
func getBlockActionsFromDb(db *gorm.DB, block uint64) ([]Action, error) {
var txs []Action
result := db.Table("action").Where("block = ?", block).Find(&txs)
return txs, result.Error
}
type dbLog struct {
logger *slog.Logger
}
func (l *dbLog) Printf(format string, args ...interface{}) {
l.logger.Info(fmt.Sprintf(format, args...))
}
func newDbLog() *dbLog {
return &dbLog{logger: slog.Default()}
}
func NewGorm(dsn string) *gorm.DB {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.New(newDbLog(), logger.Config{
Colorful: false,
LogLevel: logger.Warn,
SlowThreshold: time.Second * 10,
IgnoreRecordNotFoundError: true,
}),
})
if err != nil {
panic(err)
}
return db
}
func compareTxs(dbTxs []Tx, dataTxs []Tx) (diff int, missing int) {
dataByHash := make(map[string][]Tx, len(dataTxs))
for _, tx := range dataTxs {
dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)] = append(dataByHash[fmt.Sprintf("%s-%d", tx.TxHash, tx.TxIndex)], tx)
}
for _, dbTx := range dbTxs {
candidates := dataByHash[fmt.Sprintf("%s-%d", dbTx.TxHash, dbTx.TxIndex)]
if len(candidates) == 0 {
missing++
log.Printf("missing tx: %s", txCompareString(dbTx))
continue
}
matched := false
for _, dataTx := range candidates {
if txEqualWithoutHash(dbTx, dataTx) {
matched = true
break
}
}
if !matched {
diff++
log.Printf("tx diff hash=%s, program=%s, event:%s: %s, ", dbTx.TxHash, dbTx.Program, dbTx.Event, txCompareDiffString(dbTx, candidates[0]))
}
}
log.Printf("compare txs done: db=%d parsed=%d missing=%d diff=%d", len(dbTxs), len(dataTxs), missing, diff)
return diff, missing
}
func withinOnePercentDecimal(a decimal.Decimal, b decimal.Decimal) bool {
if a.IsZero() {
return b.IsZero()
}
diff := a.Sub(b).Abs()
threshold := a.Abs().Mul(decimal.NewFromFloat(0.03))
return diff.LessThanOrEqual(threshold)
}
func withinOnePercentStringDecimal(a string, b string) bool {
ad, errA := decimal.NewFromString(a)
bd, errB := decimal.NewFromString(b)
if errA != nil || errB != nil {
return a == b
}
return withinOnePercentDecimal(ad, bd)
}
func txEqualWithoutHash(a Tx, b Tx) bool {
//mevMatch := a.MevAgent == b.MevAgent || (a.MevAgent == "none" && b.MevAgent == "unknown") || (a.MevAgent == "unknown" && b.MevAgent == "none")
//mevNone := a.MevAgent == "none" || a.MevAgent == "unknown"
return a.PairAddress == b.PairAddress &&
a.Token1Address == b.Token1Address &&
(a.Token0Address == "" || a.Token0Address == b.Token0Address) &&
//a.Maker == b.Maker &&
(a.Token0Address == "" || withinOnePercentDecimal(a.Token0Amount, b.Token0Amount)) &&
withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) &&
a.Block == b.Block &&
a.BlockIndex == b.BlockIndex &&
a.Event == b.Event &&
a.TxIndex == b.TxIndex &&
a.Program == b.Program &&
(a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0)) &&
(a.Program == solana_parser.SolProgramPumpAMM || a.Program == solana_parser.SolProgramPump || a.Program == solana_parser.SolProgramMeteoraPools || (a.Event == "create") || withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1)) &&
// a.PositionChange == b.PositionChange &&
(a.Platform == b.Platform || (a.Platform == "photon" && b.Platform == "fake") || (a.Platform == "trojan" && b.Platform == "fake")) &&
a.CUPrice.String() == b.CUPrice.String() // &&
//mevMatch &&
//(mevNone || a.MevAgentFee.String() == b.MevAgentFee.String()) &&
//(a.Program == solana_parser.SolProgramRaydiumV4 || a.AfterSOLBalance.String() == b.AfterSOLBalance.String())
//&&
// a.EntryContract == b.EntryContract
}
func txCompareDiffString(a Tx, b Tx) string {
var diffs []string
if a.PairAddress != b.PairAddress {
diffs = append(diffs, fmt.Sprintf("PairAddress db=%s data=%s", a.PairAddress, b.PairAddress))
}
//if a.Maker != b.Maker {
// diffs = append(diffs, fmt.Sprintf("Maker db=%s, data=%s", a.Maker, b.Maker))
//}
if a.Token1Address != b.Token1Address {
diffs = append(diffs, fmt.Sprintf("Token1Address db=%s data=%s", a.Token1Address, b.Token1Address))
}
if a.Token0Address != b.Token0Address {
diffs = append(diffs, fmt.Sprintf("Token0Address db=%s data=%s", a.Token0Address, b.Token0Address))
}
if !withinOnePercentDecimal(a.Token0Amount, b.Token0Amount) {
diffs = append(diffs, fmt.Sprintf("Token0Amount db=%s data=%s", a.Token0Amount.String(), b.Token0Amount.String()))
}
if !withinOnePercentDecimal(a.Token1Amount, b.Token1Amount) {
diffs = append(diffs, fmt.Sprintf("Token1Amount db=%s data=%s", a.Token1Amount.String(), b.Token1Amount.String()))
}
if a.Block != b.Block {
diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block))
}
if a.BlockIndex != b.BlockIndex {
diffs = append(diffs, fmt.Sprintf("BlockIndex db=%d data=%d", a.BlockIndex, b.BlockIndex))
}
if a.Event != b.Event {
diffs = append(diffs, fmt.Sprintf("Event db=%s data=%s", a.Event, b.Event))
}
if a.TxIndex != b.TxIndex {
diffs = append(diffs, fmt.Sprintf("TxIndex db=%d data=%d", a.TxIndex, b.TxIndex))
}
if a.Program != b.Program {
diffs = append(diffs, fmt.Sprintf("Program db=%s data=%s", a.Program, b.Program))
}
if !withinOnePercentStringDecimal(a.AfterReserve0, b.AfterReserve0) {
diffs = append(diffs, fmt.Sprintf("AfterReserve0 db=%s data=%s", a.AfterReserve0, b.AfterReserve0))
}
if !withinOnePercentStringDecimal(a.AfterReserve1, b.AfterReserve1) {
diffs = append(diffs, fmt.Sprintf("AfterReserve1 db=%s data=%s", a.AfterReserve1, b.AfterReserve1))
}
//if a.PositionChange != b.PositionChange {
// diffs = append(diffs, fmt.Sprintf("PositionChange db=%d data=%d", a.PositionChange, b.PositionChange))
//}
if a.Platform != b.Platform {
diffs = append(diffs, fmt.Sprintf("Platform db=%s data=%s", a.Platform, b.Platform))
}
if a.CUPrice.String() != b.CUPrice.String() {
diffs = append(diffs, fmt.Sprintf("CUPrice db=%s data=%s", a.CUPrice.String(), b.CUPrice.String()))
}
//if a.MevAgent != b.MevAgent {
// diffs = append(diffs, fmt.Sprintf("MevAgent db=%s data=%s", a.MevAgent, b.MevAgent))
//}
//if a.MevAgentFee.String() != b.MevAgentFee.String() {
// diffs = append(diffs, fmt.Sprintf("MevAgentFee db=%s data=%s", a.MevAgentFee.String(), b.MevAgentFee.String()))
//}
//if a.AfterSOLBalance.String() != b.AfterSOLBalance.String() {
// diffs = append(diffs, fmt.Sprintf("AfterSOLBalance db=%s data=%s", a.AfterSOLBalance.String(), b.AfterSOLBalance.String()))
//}
//if a.EntryContract != b.EntryContract {
// diffs = append(diffs, fmt.Sprintf("EntryContract db=%s data=%s", a.EntryContract, b.EntryContract))
//}
return strings.Join(diffs, "; ")
}
func compareActions(dbActions []Action, dataActions []Action) (diff, missing int) {
dataByHash := make(map[string][]Action, len(dataActions))
for _, action := range dataActions {
dataByHash[action.TxHash] = append(dataByHash[action.TxHash], action)
}
for _, dbAction := range dbActions {
candidates := dataByHash[dbAction.TxHash]
if len(candidates) == 0 {
missing++
log.Printf("missing action: %s", actionCompareString(dbAction))
continue
}
matched := false
for _, dataAction := range candidates {
if actionEqualWithoutHash(dbAction, dataAction) {
matched = true
break
}
}
if !matched {
diff++
log.Printf("action diff hash=%s: %s", dbAction.TxHash, actionCompareDiffString(dbAction, candidates[0]))
}
}
log.Printf("compare actions done: db=%d parsed=%d missing=%d diff=%d", len(dbActions), len(dataActions), missing, diff)
return diff, missing
}
func actionEqualWithoutHash(a Action, b Action) bool {
return a.Maker == b.Maker &&
a.Token == b.Token &&
a.Pair == b.Pair &&
a.Action == b.Action &&
a.Block == b.Block
}
func actionCompareDiffString(a Action, b Action) string {
var diffs []string
if a.Maker != b.Maker {
diffs = append(diffs, fmt.Sprintf("Maker db=%s data=%s", a.Maker, b.Maker))
}
if a.Token != b.Token {
diffs = append(diffs, fmt.Sprintf("Token db=%s data=%s", a.Token, b.Token))
}
if a.Pair != b.Pair {
diffs = append(diffs, fmt.Sprintf("Pair db=%s data=%s", a.Pair, b.Pair))
}
if a.Action != b.Action {
diffs = append(diffs, fmt.Sprintf("Action db=%s data=%s", a.Action, b.Action))
}
if a.Block != b.Block {
diffs = append(diffs, fmt.Sprintf("Block db=%d data=%d", a.Block, b.Block))
}
return strings.Join(diffs, "; ")
}
func actionCompareString(action Action) string {
return fmt.Sprintf("Maker=%s Token=%s Pair=%s Action=%s Block=%d TxHash=%s", action.Maker, action.Token, action.Pair, action.Action, action.Block, action.TxHash)
}
func txCompareString(tx Tx) string {
return fmt.Sprintf(
"tx.Program=%s TxHash=%s PairAddress=%s Token1Address=%s Token0Amount=%s Token1Amount=%s Block=%d BlockIndex=%d Event=%s TxIndex=%d AfterReserve0=%s AfterReserve1=%s PositionChange=%d Platform=%s CUPrice=%s MevAgent=%s MevAgentFee=%s AfterSOLBalance=%s EntryContract=%s",
tx.Program,
tx.TxHash,
tx.PairAddress,
tx.Token1Address,
tx.Token0Amount.String(),
tx.Token1Amount.String(),
tx.Block,
tx.BlockIndex,
tx.Event,
tx.TxIndex,
tx.AfterReserve0,
tx.AfterReserve1,
tx.PositionChange,
tx.Platform,
tx.CUPrice.String(),
tx.MevAgent,
tx.MevAgentFee.String(),
tx.AfterSOLBalance.String(),
tx.EntryContract,
)
} }

View File

@@ -26,7 +26,7 @@ func main() {
var data = NewBlockData(decimal.NewFromFloat(100.0)) var data = NewBlockData(decimal.NewFromFloat(100.0))
client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d") client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d")
var version uint64 = 0 var version uint64 = 0
txSig, _ := solana.SignatureFromBase58("2LCw5yZy6sGTWKpJNxpFxR11M66cXPsrGmJXnQmWW9QVv6SDWRmu1aevc6yE9NeUz78mFb4T8TEx9w5781NHnz2T") txSig, _ := solana.SignatureFromBase58("4sj82GCLtgTDExq7B8YrBsrrqPcE4FqT5Y1gKWmE4cHMDxs7wkCV1hik73dSZ99gZm3K4wyBZQ6U8Nmf48rM9Jri")
tx, err := client.GetTransaction(context.Background(), txSig, &rpc.GetTransactionOpts{ tx, err := client.GetTransaction(context.Background(), txSig, &rpc.GetTransactionOpts{
Commitment: rpc.CommitmentFinalized, Commitment: rpc.CommitmentFinalized,
Encoding: solana.EncodingBase64, Encoding: solana.EncodingBase64,
@@ -78,6 +78,10 @@ func main() {
i++ i++
} }
} }
fmt.Printf("swap: %d, program: %s, event: %s, base: %s quote: %s, base amount: %s, quote amount: %s, \n", i,
action.Program, action.Event, action.BaseMint.String(), action.QuoteMint.String(),
action.BaseAmount.String(),
action.QuoteAmount.String())
if err = HandleAction(context.Background(), result, actions, data); err != nil { if err = HandleAction(context.Background(), result, actions, data); err != nil {
//h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err) //h.logger.Errorf("handle action error: %s - %v", result.RawTx.Transaction.Signatures[0].String(), err)
fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err) fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err)

78
meta.go
View File

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

View File

@@ -66,6 +66,13 @@ type dlmmPositionCloseEvent struct {
Owner solana.PublicKey Owner solana.PublicKey
} }
type dlmmLbPairCreateEvent struct {
LbPair solana.PublicKey
BinStep uint16
TokenX solana.PublicKey
TokenY solana.PublicKey
}
type dlmmClaimFeeInnerEvent struct { type dlmmClaimFeeInnerEvent struct {
LbPair solana.PublicKey LbPair solana.PublicKey
Position solana.PublicKey Position solana.PublicKey
@@ -117,6 +124,11 @@ type dlmmBinLiquidityDistribution struct {
DistributionY uint16 DistributionY uint16
} }
type dlmmBinLiquidityDistributionByWeight struct {
BinId int32
Weight uint16
}
type dlmmBinLiquidityReduction struct { type dlmmBinLiquidityReduction struct {
BinId int32 BinId int32
BpsToRemove uint16 BpsToRemove uint16
@@ -143,6 +155,14 @@ type dlmmLiquidityParameterByStrategy struct {
StrategyParameters dlmmStrategyParameters StrategyParameters dlmmStrategyParameters
} }
type dlmmLiquidityParameterByWeight struct {
AmountX uint64
AmountY uint64
ActiveID int32
MaxActiveBinSlippage int32
BinLiquidityDist []dlmmBinLiquidityDistributionByWeight
}
type dlmmAddLiquidityArgs struct { type dlmmAddLiquidityArgs struct {
LiquidityParameter dlmmLiquidityParameter LiquidityParameter dlmmLiquidityParameter
} }
@@ -161,6 +181,57 @@ type dlmmAddLiquidityByStrategy2Args struct {
RemainingAccountsInfo dlmmRemainingAccountsInfo RemainingAccountsInfo dlmmRemainingAccountsInfo
} }
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 { type dlmmRemoveLiquidityArgs struct {
BinLiquidityRemoval []dlmmBinLiquidityReduction BinLiquidityRemoval []dlmmBinLiquidityReduction
} }
@@ -247,6 +318,16 @@ type dlmmLiquidityAccounts struct {
tokenYProgramIdx int tokenYProgramIdx int
} }
type dlmmOneSideLiquidityAccounts struct {
positionIdx int
poolIdx int
userTokenIdx int
reserveIdx int
tokenMintIdx int
userIdx int
tokenProgramIdx int
}
var meteoraDlmmEventAuthority = func() solana.PublicKey { var meteoraDlmmEventAuthority = func() solana.PublicKey {
key, _, err := solana.FindProgramAddress([][]byte{[]byte("__event_authority")}, meteoraDlmmProgram) key, _, err := solana.FindProgramAddress([][]byte{[]byte("__event_authority")}, meteoraDlmmProgram)
if err != nil { if err != nil {
@@ -266,7 +347,11 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI
discriminator := *(*[8]byte)(decode[:8]) discriminator := *(*[8]byte)(decode[:8])
switch discriminator { switch discriminator {
case meteoraInitializeLbPairDiscriminator: case meteoraInitializeCustomizablePermissionlessLbPairDiscriminator,
meteoraInitializeCustomizablePermissionlessLbPair2Discriminator,
meteoraInitializeLbPairDiscriminator,
meteoraInitializeLbPair2Discriminator,
meteoraInitializePermissionLbPairDiscriminator:
return metaoradlmmInitializeParser(tx, instruction, innerInstructions, offset) return metaoradlmmInitializeParser(tx, instruction, innerInstructions, offset)
case meteoraDlmmInitializePositionDiscriminator, meteoraDlmmInitializePosition2Discriminator, case meteoraDlmmInitializePositionDiscriminator, meteoraDlmmInitializePosition2Discriminator,
meteoraDlmmInitializePositionByOperatorDiscriminator, meteoraDlmmInitializePositionPdaDiscriminator: meteoraDlmmInitializePositionByOperatorDiscriminator, meteoraDlmmInitializePositionPdaDiscriminator:
@@ -276,13 +361,16 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI
case meteoraDlmmSwap2Discriminator, meteoraDlmmSwapExactOut2Discriminator, meteoraDlmmSwapWithPriceImpact2Discriminator: case meteoraDlmmSwap2Discriminator, meteoraDlmmSwapExactOut2Discriminator, meteoraDlmmSwapWithPriceImpact2Discriminator:
return metaoradlmmSwap2Parser(tx, instruction, innerInstructions, offset) return metaoradlmmSwap2Parser(tx, instruction, innerInstructions, offset)
case meteoraDlmmAddLiquidityDiscriminator, meteoraDlmmAddLiquidity2Discriminator, case meteoraDlmmAddLiquidityDiscriminator, meteoraDlmmAddLiquidity2Discriminator,
meteoraDlmmAddLiquidityByStrategyDiscriminator, meteoraDlmmAddLiquidityByStrategy2Discriminator: meteoraDlmmAddLiquidityByStrategyDiscriminator, meteoraDlmmAddLiquidityByStrategy2Discriminator,
meteoraDlmmAddLiquidityByWeightDiscriminator, meteoraDlmmAddLiquidityOneSideDiscriminator,
meteoraDlmmAddLiquidityOneSidePreciseDiscriminator, meteoraDlmmAddLiquidityOneSidePrecise2Discriminator,
meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator:
return metaoradlmmAddLiquidityParser(tx, instruction, innerInstructions, offset) return metaoradlmmAddLiquidityParser(tx, instruction, innerInstructions, offset)
case meteoraDlmmClaimFeeDiscriminator, meteoraDlmmClaimFee2Discriminator: case meteoraDlmmClaimFeeDiscriminator, meteoraDlmmClaimFee2Discriminator:
return metaoradlmmClaimFeeParser(tx, instruction, innerInstructions, offset) return metaoradlmmClaimFeeParser(tx, instruction, innerInstructions, offset)
case meteoraDlmmRebalanceLiquidityDiscriminator: case meteoraDlmmRebalanceLiquidityDiscriminator:
return metaoradlmmRebalanceLiquidityParser(tx, instruction, innerInstructions, offset) return metaoradlmmRebalanceLiquidityParser(tx, instruction, innerInstructions, offset)
case meteoraDlmmRemoveLiquidityDiscriminator, meteoraDlmmRemoveLiquidity2Discriminator, case meteoraDlmmRemoveAllLiquidityDiscriminator, meteoraDlmmRemoveLiquidityDiscriminator, meteoraDlmmRemoveLiquidity2Discriminator,
meteoraDlmmRemoveLiquidityByRangeDiscriminator, meteoraDlmmRemoveLiquidityByRange2Discriminator: meteoraDlmmRemoveLiquidityByRangeDiscriminator, meteoraDlmmRemoveLiquidityByRange2Discriminator:
return metaoradlmmRemoveLiquidityParser(tx, instruction, innerInstructions, offset) return metaoradlmmRemoveLiquidityParser(tx, instruction, innerInstructions, offset)
case meteoraDlmmClosePositionDiscriminator, meteoraDlmmClosePosition2Discriminator, meteoraDlmmClosePositionIfEmptyDiscriminator: case meteoraDlmmClosePositionDiscriminator, meteoraDlmmClosePosition2Discriminator, meteoraDlmmClosePositionIfEmptyDiscriminator:
@@ -292,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) { func metaoradlmmInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
market := tx.rawTx.accountList[instruction.Accounts[0]] accounts, err := resolveDlmmInitializeAccounts(tx.rawTx, instruction.Data, instruction.Accounts)
token0 := tx.rawTx.accountList[instruction.Accounts[2]] if err != nil {
token1 := tx.rawTx.accountList[instruction.Accounts[3]] 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] entryContract := tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
var baseDecimals uint8 findMintDecimals := func(mint solana.PublicKey) uint8 {
var quoteDecimals uint8 for _, acc := range tx.rawTx.Meta.PostTokenBalances {
for _, acc := range tx.rawTx.Meta.PostTokenBalances { if acc.MintAccount.Equals(mint) {
if acc.MintAccount.Equals(token0) { return uint8(acc.UITokenAmount.Decimals)
baseDecimals = uint8(acc.UITokenAmount.Decimals) }
}
if acc.MintAccount.Equals(token1) {
quoteDecimals = uint8(acc.UITokenAmount.Decimals)
} }
return 0
} }
swap := Swap{ swap := Swap{
Program: SolProgramMeteoraDLMM, Program: SolProgramMeteoraDLMM,
Event: "create", Event: "create",
Pool: market, Pool: accounts.pool,
BaseMint: token0, BaseMint: accounts.token0,
QuoteMint: token1, QuoteMint: accounts.token1,
BaseTokenProgram: tx.rawTx.accountList[instruction.Accounts[11]], BaseTokenProgram: accounts.baseTokenProgram,
QuoteTokenProgram: tx.rawTx.accountList[instruction.Accounts[12]], QuoteTokenProgram: accounts.quoteTokenProgram,
Creator: tx.rawTx.accountList[0], Creator: tx.rawTx.accountList[0],
BaseMintDecimals: baseDecimals, BaseMintDecimals: findMintDecimals(accounts.token0),
QuoteMintDecimals: quoteDecimals, QuoteMintDecimals: findMintDecimals(accounts.token1),
User: tx.rawTx.accountList[instruction.Accounts[8]], User: accounts.user,
EntryContract: entryContract, EntryContract: entryContract,
} }
var prefixLen = offset[1] createEvent, nextOffset, found, err := dlmmLbPairCreateEventFromInnerInstructions(innerInstructions, instruction, offset)
inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil { 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 if found {
offset = nextOffset
for innerIndex, innerInstr := range inners { if !createEvent.LbPair.IsZero() {
if innerInstr.ProgramIDIndex == programIndex && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraInitializeLbPairEventDiscriminator[:]) { swap.Pool = createEvent.LbPair
if offset[1] == 0 {
offset[0] += 1
} else {
offset[1] = uint(innerIndex) + 1 + prefixLen
}
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 return []Swap{swap}, offset, nil
} }
@@ -392,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]) 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 { if err != nil {
return nil, nextOffset, err return nil, nextOffset, err
} }
if !found {
return nil, nextOffset, InstructionIgnoredError
}
offset = nextOffset offset = nextOffset
if !createEvent.LbPair.IsZero() { if !createEvent.LbPair.IsZero() {
@@ -497,22 +666,33 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
} }
discriminator := *(*[8]byte)(decode[:8]) discriminator := *(*[8]byte)(decode[:8])
var swapMode SwapMode
var fixedAmount decimal.Decimal
var limitAmount decimal.Decimal
switch discriminator { switch discriminator {
case meteoraDlmmSwapDiscriminator, meteoraDlmmSwap2Discriminator: case meteoraDlmmSwapDiscriminator, meteoraDlmmSwap2Discriminator:
var args meteoraDlmmSwapArgs var args meteoraDlmmSwapArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { 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]) 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: case meteoraDlmmSwapExactOutDiscriminator, meteoraDlmmSwapExactOut2Discriminator:
var args meteoraDlmmSwapExactOutArgs var args meteoraDlmmSwapExactOutArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { 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]) 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: case meteoraDlmmSwapWithPriceImpactDiscriminator, meteoraDlmmSwapWithPriceImpact2Discriminator:
var args meteoraDlmmSwapWithPriceImpactArgs var args meteoraDlmmSwapWithPriceImpactArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { 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]) 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: default:
return nil, increaseOffset(offset), InstructionIgnoredError return nil, increaseOffset(offset), InstructionIgnoredError
} }
@@ -649,6 +829,18 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
userQuote = userQuote.Add(decimal.NewFromUint64(solAmount)) 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{ swap := Swap{
Program: SolProgramMeteoraDLMM, Program: SolProgramMeteoraDLMM,
@@ -664,6 +856,13 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
User: eventUser, User: eventUser,
BaseAmount: baseAmount, BaseAmount: baseAmount,
QuoteAmount: quoteAmount, QuoteAmount: quoteAmount,
FeeAmount: feeAmount,
FeeBps: dlmmSwapFeeBpsString(swapEvent.FeeBps),
LpFeeAmount: lpFeeAmount,
FeeSide: feeSide,
FeeMint: feeMint,
FeeTokenProgram: feeTokenProgram,
FeeMintDecimals: feeDecimals,
BaseReserve: baseReserve, BaseReserve: baseReserve,
QuoteReserve: quoteReserve, QuoteReserve: quoteReserve,
UserBaseBalance: userBase, UserBaseBalance: userBase,
@@ -673,6 +872,7 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
StartBinId: swapEvent.StartBinId, StartBinId: swapEvent.StartBinId,
EndBinId: swapEvent.EndBinId, EndBinId: swapEvent.EndBinId,
} }
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
return []Swap{swap}, offset, nil return []Swap{swap}, offset, nil
} }
@@ -681,6 +881,39 @@ func metaoradlmmSwap2Parser(tx *Tx, instruction Instruction, innerInstructions I
return metaoradlmmSwapParser(tx, instruction, innerInstructions, offset) 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) { func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
result := tx.rawTx result := tx.rawTx
@@ -705,9 +938,10 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountX uint64 amountX uint64
amountY uint64 amountY uint64
binDist []dlmmBinLiquidityDistribution binDist []dlmmBinLiquidityDistribution
weightDist []dlmmBinLiquidityDistributionByWeight
startBinId int32 startBinId int32
endBinId int32 endBinId int32
hasRange bool oneSide bool
) )
switch discriminator { switch discriminator {
@@ -720,7 +954,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountY = args.LiquidityParameter.AmountY amountY = args.LiquidityParameter.AmountY
binDist = args.LiquidityParameter.BinLiquidityDist binDist = args.LiquidityParameter.BinLiquidityDist
startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist) startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist)
hasRange = len(binDist) > 0
case meteoraDlmmAddLiquidity2Discriminator: case meteoraDlmmAddLiquidity2Discriminator:
var args dlmmAddLiquidity2Args var args dlmmAddLiquidity2Args
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
@@ -730,7 +963,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountY = args.LiquidityParameter.AmountY amountY = args.LiquidityParameter.AmountY
binDist = args.LiquidityParameter.BinLiquidityDist binDist = args.LiquidityParameter.BinLiquidityDist
startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist) startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist)
hasRange = len(binDist) > 0
case meteoraDlmmAddLiquidityByStrategyDiscriminator: case meteoraDlmmAddLiquidityByStrategyDiscriminator:
var args dlmmAddLiquidityByStrategyArgs var args dlmmAddLiquidityByStrategyArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
@@ -740,7 +972,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountY = args.LiquidityParameter.AmountY amountY = args.LiquidityParameter.AmountY
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
hasRange = true
case meteoraDlmmAddLiquidityByStrategy2Discriminator: case meteoraDlmmAddLiquidityByStrategy2Discriminator:
var args dlmmAddLiquidityByStrategy2Args var args dlmmAddLiquidityByStrategy2Args
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
@@ -750,16 +981,49 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountY = args.LiquidityParameter.AmountY amountY = args.LiquidityParameter.AmountY
startBinId = args.LiquidityParameter.StrategyParameters.MinBinId startBinId = args.LiquidityParameter.StrategyParameters.MinBinId
endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId
hasRange = true case meteoraDlmmAddLiquidityByWeightDiscriminator:
var args dlmmAddLiquidityByWeightArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity by weight decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
amountX = args.LiquidityParameter.AmountX
amountY = args.LiquidityParameter.AmountY
weightDist = args.LiquidityParameter.BinLiquidityDist
startBinId, endBinId = dlmmMinMaxBinIdFromWeightDistribution(weightDist)
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: default:
return nil, increaseOffset(offset), InstructionIgnoredError 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) addEvent, nextOffset, err := dlmmAddLiquidityEventFromInnerInstructions(innerInstructions, instruction, offset)
if err != nil { if err != nil {
return nil, nextOffset, err return nil, nextOffset, err
@@ -768,11 +1032,17 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
amountX = addEvent.Amounts[0] amountX = addEvent.Amounts[0]
amountY = addEvent.Amounts[1] amountY = addEvent.Amounts[1]
binChanges := []DlmmBinLiquidityChange(nil) if oneSide {
if len(binDist) > 0 { swaps, err := dlmmBuildOneSideAddSwap(tx, instruction, addEvent, startBinId, endBinId, entryContract)
binChanges = dlmmBinChangesFromDistribution(amountX, amountY, binDist) if err != nil {
} else if hasRange { return nil, offset, err
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, 0) }
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] pool := result.accountList[accounts.poolIdx]
@@ -805,7 +1075,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
baseAmount = amountYDec baseAmount = amountYDec
quoteAmount = amountXDec quoteAmount = amountXDec
} }
eventUser := result.accountList[accounts.userIdx] eventUser := result.accountList[accounts.userIdx]
if !addEvent.From.IsZero() { if !addEvent.From.IsZero() {
eventUser = addEvent.From eventUser = addEvent.From
@@ -866,7 +1135,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc
ActiveBinId: addEvent.ActiveBinId, ActiveBinId: addEvent.ActiveBinId,
StartBinId: startBinId, StartBinId: startBinId,
EndBinId: endBinId, EndBinId: endBinId,
BinChanges: binChanges,
PositionAccount: result.accountList[accounts.positionIdx], PositionAccount: result.accountList[accounts.positionIdx],
} }
@@ -894,19 +1162,19 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
discriminator := *(*[8]byte)(decode[:8]) discriminator := *(*[8]byte)(decode[:8])
var ( var (
binChanges []DlmmBinLiquidityChange
startBinId int32 startBinId int32
endBinId int32 endBinId int32
removeBp int32 removeBp int32
) )
switch discriminator { switch discriminator {
case meteoraDlmmRemoveAllLiquidityDiscriminator:
removeBp = 10000
case meteoraDlmmRemoveLiquidityDiscriminator: case meteoraDlmmRemoveLiquidityDiscriminator:
var args dlmmRemoveLiquidityArgs var args dlmmRemoveLiquidityArgs
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { 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]) 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) startBinId, endBinId = dlmmMinMaxBinIdFromReduction(args.BinLiquidityRemoval)
removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval) removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval)
case meteoraDlmmRemoveLiquidity2Discriminator: case meteoraDlmmRemoveLiquidity2Discriminator:
@@ -914,7 +1182,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { 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]) 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) startBinId, endBinId = dlmmMinMaxBinIdFromReduction(args.BinLiquidityRemoval)
removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval) removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval)
case meteoraDlmmRemoveLiquidityByRangeDiscriminator: case meteoraDlmmRemoveLiquidityByRangeDiscriminator:
@@ -925,7 +1192,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
startBinId = args.FromBinId startBinId = args.FromBinId
endBinId = args.ToBinId endBinId = args.ToBinId
removeBp = int32(args.BpsToRemove) removeBp = int32(args.BpsToRemove)
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, args.BpsToRemove)
case meteoraDlmmRemoveLiquidityByRange2Discriminator: case meteoraDlmmRemoveLiquidityByRange2Discriminator:
var args dlmmRemoveLiquidityByRange2Args var args dlmmRemoveLiquidityByRange2Args
if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil {
@@ -934,7 +1200,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
startBinId = args.FromBinId startBinId = args.FromBinId
endBinId = args.ToBinId endBinId = args.ToBinId
removeBp = int32(args.BpsToRemove) removeBp = int32(args.BpsToRemove)
binChanges = dlmmBinChangesFromRange(startBinId, endBinId, args.BpsToRemove)
default: default:
return nil, increaseOffset(offset), InstructionIgnoredError return nil, increaseOffset(offset), InstructionIgnoredError
} }
@@ -980,7 +1245,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
baseAmount = amountYDec baseAmount = amountYDec
quoteAmount = amountXDec quoteAmount = amountXDec
} }
eventUser := result.accountList[accounts.userIdx] eventUser := result.accountList[accounts.userIdx]
if !removeEvent.From.IsZero() { if !removeEvent.From.IsZero() {
eventUser = removeEvent.From eventUser = removeEvent.From
@@ -1042,7 +1306,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst
StartBinId: startBinId, StartBinId: startBinId,
EndBinId: endBinId, EndBinId: endBinId,
RemoveBp: removeBp, RemoveBp: removeBp,
BinChanges: binChanges,
PositionAccount: result.accountList[accounts.positionIdx], PositionAccount: result.accountList[accounts.positionIdx],
} }
@@ -1286,7 +1549,6 @@ func metaoradlmmRebalanceLiquidityParser(tx *Tx, instruction Instruction, innerI
ActiveBinId: event.ActiveBinId, ActiveBinId: event.ActiveBinId,
StartBinId: event.OldMinBinId, StartBinId: event.OldMinBinId,
EndBinId: event.OldMaxBinId, EndBinId: event.OldMaxBinId,
BinChanges: dlmmBinChangesFromRange(event.OldMinBinId, event.OldMaxBinId, 0),
PositionAccount: result.accountList[accounts.positionIdx], PositionAccount: result.accountList[accounts.positionIdx],
}) })
} }
@@ -1312,7 +1574,6 @@ func metaoradlmmRebalanceLiquidityParser(tx *Tx, instruction Instruction, innerI
ActiveBinId: event.ActiveBinId, ActiveBinId: event.ActiveBinId,
StartBinId: event.NewMinBinId, StartBinId: event.NewMinBinId,
EndBinId: event.NewMaxBinId, EndBinId: event.NewMaxBinId,
BinChanges: dlmmBinChangesFromRange(event.NewMinBinId, event.NewMaxBinId, 0),
PositionAccount: result.accountList[accounts.positionIdx], PositionAccount: result.accountList[accounts.positionIdx],
}) })
} }
@@ -1446,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) 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] var prefixLen = offset[1]
inners, err := getInnerInstructions(innerInstructions, prefixLen) inners, err := getInnerInstructions(innerInstructions, prefixLen)
if err != nil { 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 { for innerIndex, innerInstr := range inners {
if innerInstr.ProgramIDIndex != instruction.ProgramIDIndex { if innerInstr.ProgramIDIndex != instruction.ProgramIDIndex {
@@ -1465,9 +1726,9 @@ func dlmmPositionCreateEventFromInnerInstructions(innerInstructions InnerInstruc
} else { } else {
offset[1] = uint(innerIndex) + 1 + prefixLen 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) { func dlmmPositionCloseEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmPositionCloseEvent, [2]uint, bool, error) {
@@ -1494,6 +1755,51 @@ func dlmmPositionCloseEventFromInnerInstructions(innerInstructions InnerInstruct
return dlmmPositionCloseEvent{}, increaseOffset(offset), false, nil 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) { func dlmmDecodeAddLiquidityEvent(data []byte) (dlmmAddLiquidityEvent, bool) {
switch { switch {
case len(data) >= 8 && bytes.Equal(data[:8], meteoraDlmmAddLiquidityEventDiscriminator[:]): case len(data) >= 8 && bytes.Equal(data[:8], meteoraDlmmAddLiquidityEventDiscriminator[:]):
@@ -1787,6 +2093,154 @@ func resolveDlmmLiquidityAccounts(result *RawTx, accounts []int) (dlmmLiquidityA
}, nil }, 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) { func resolveDlmmClaimFeeAccounts(result *RawTx, data []byte, accounts []int) (dlmmLiquidityAccounts, error) {
if len(data) < 8 { if len(data) < 8 {
return dlmmLiquidityAccounts{}, fmt.Errorf("instruction data too short") return dlmmLiquidityAccounts{}, fmt.Errorf("instruction data too short")
@@ -1958,56 +2412,67 @@ func dlmmTokenBalanceMeta(result *RawTx, accountIndex int) (TokenBalance, bool)
return TokenBalance{}, false return TokenBalance{}, false
} }
func dlmmBinChangesFromDistribution(amountX, amountY uint64, dist []dlmmBinLiquidityDistribution) []DlmmBinLiquidityChange { func dlmmAllocateByWeights(total uint64, weights []uint64) []decimal.Decimal {
if len(dist) == 0 { if len(weights) == 0 {
return nil return nil
} }
totalX := decimal.NewFromUint64(amountX)
totalY := decimal.NewFromUint64(amountY) sumWeights := uint64(0)
denom := decimal.NewFromInt(10000) for _, weight := range weights {
changes := make([]DlmmBinLiquidityChange, 0, len(dist)) sumWeights += weight
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,
})
} }
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 { func dlmmApplySignedAllocation(values []decimal.Decimal, negative bool) []decimal.Decimal {
if len(reduction) == 0 { if !negative {
return nil return values
} }
changes := make([]DlmmBinLiquidityChange, 0, len(reduction)) out := make([]decimal.Decimal, len(values))
for _, item := range reduction { for i, value := range values {
changes = append(changes, DlmmBinLiquidityChange{ out[i] = value.Neg()
BinId: item.BinId,
BpsToRemove: item.BpsToRemove,
})
} }
return changes return out
} }
func dlmmBinChangesFromRange(startBinId, endBinId int32, bpsToRemove uint16) []DlmmBinLiquidityChange { func dlmmMinMaxBinIDFromCompressedDeposits(bins []dlmmCompressedBinDepositAmount) (startBinID, endBinID int32) {
if startBinId > endBinId { if len(bins) == 0 {
startBinId, endBinId = endBinId, startBinId return 0, 0
} }
count := int(endBinId-startBinId) + 1 startBinID = bins[0].BinID
if count <= 0 { endBinID = bins[0].BinID
return nil for _, bin := range bins[1:] {
if bin.BinID < startBinID {
startBinID = bin.BinID
}
if bin.BinID > endBinID {
endBinID = bin.BinID
}
} }
changes := make([]DlmmBinLiquidityChange, 0, count) return startBinID, endBinID
for binId := startBinId; binId <= endBinId; binId++ {
changes = append(changes, DlmmBinLiquidityChange{
BinId: binId,
BpsToRemove: bpsToRemove,
})
}
return changes
} }
func dlmmCommonRemoveBp(reduction []dlmmBinLiquidityReduction) int32 { func dlmmCommonRemoveBp(reduction []dlmmBinLiquidityReduction) int32 {
@@ -2047,6 +2512,23 @@ func dlmmMinMaxBinIdFromDistribution(dist []dlmmBinLiquidityDistribution) (int32
return min, max return min, max
} }
func dlmmMinMaxBinIdFromWeightDistribution(dist []dlmmBinLiquidityDistributionByWeight) (int32, int32) {
if len(dist) == 0 {
return 0, 0
}
min := dist[0].BinId
max := dist[0].BinId
for _, item := range dist[1:] {
if item.BinId < min {
min = item.BinId
}
if item.BinId > max {
max = item.BinId
}
}
return min, max
}
func dlmmMinMaxBinIdFromReduction(reduction []dlmmBinLiquidityReduction) (int32, int32) { func dlmmMinMaxBinIdFromReduction(reduction []dlmmBinLiquidityReduction) (int32, int32) {
if len(reduction) == 0 { if len(reduction) == 0 {
return 0, 0 return 0, 0

424
metaoradlmm_test.go Normal file
View File

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

View File

@@ -15,6 +15,11 @@ type metaoraPoolInitializePoolData struct {
TokenBAmount uint64 `json:"tokenBAmount"` TokenBAmount uint64 `json:"tokenBAmount"`
} }
type metaoraPoolSwapArgs struct {
InAmount uint64
MinimumOutAmount uint64
}
var ( var (
meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi") meteoraVaultProgram = solana.MustPublicKeyFromBase58("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi")
meteoraVaultDepositDiscriminator = []byte{0xf2, 0x23, 0xc6, 0x89, 0x52, 0xe1, 0xf2, 0xb6} 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) { 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]] pool := tx.rawTx.accountList[instruction.Accounts[0]]
payer := tx.rawTx.accountList[instruction.Accounts[12]] payer := tx.rawTx.accountList[instruction.Accounts[12]]
@@ -874,5 +883,10 @@ func metaoraPoolSwap(tx *Tx, instruction Instruction, innerInstructions InnerIns
EntryContract: entryContract, EntryContract: entryContract,
}, },
} }
swaps[0].SetSwapAmountInfo(
SwapModeExactIn,
decimal.NewFromUint64(args.InAmount),
decimal.NewFromUint64(args.MinimumOutAmount),
)
return swaps, offset, nil return swaps, offset, nil
} }

View File

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

View File

@@ -188,6 +188,36 @@ type meteoraDammSwapEvent struct {
ReserveBAmount uint64 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) { func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if len(instruction.Accounts) < 9 { if len(instruction.Accounts) < 9 {
return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length") return nil, increaseOffset(offset), fmt.Errorf("invalid instruction accounts length")
@@ -276,28 +306,30 @@ func meteoraDammV2Swap(tx *Tx, instruction Instruction, innerInstructions InnerI
return nil, offset, fmt.Errorf("invalid trade direction") return nil, offset, fmt.Errorf("invalid trade direction")
} }
return []Swap{ swap := Swap{
{ Program: SolProgramMeteoraAmmV2,
Program: SolProgramMeteoraAmmV2, Event: event,
Event: event, Pool: swapEvent.Pool,
Pool: swapEvent.Pool, BaseMint: baseMint,
BaseMint: baseMint, QuoteMint: quoteMint,
QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram,
BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram,
QuoteTokenProgram: quoteTokenProgram, Creator: solana.PublicKey{},
Creator: solana.PublicKey{}, BaseMintDecimals: baseMintDecimals,
BaseMintDecimals: baseMintDecimals, QuoteMintDecimals: quoteMintDecimals,
QuoteMintDecimals: quoteMintDecimals, User: payer,
User: payer, BaseAmount: baseAmount,
BaseAmount: baseAmount, QuoteAmount: quoteAmount,
QuoteAmount: quoteAmount, BaseReserve: baseReserve,
BaseReserve: baseReserve, QuoteReserve: quoteReserve,
QuoteReserve: quoteReserve, UserBaseBalance: userBase,
UserBaseBalance: userBase, UserQuoteBalance: userQuote,
UserQuoteBalance: userQuote, EntryContract: entryContract,
EntryContract: entryContract, }
}, if swapMode, fixedAmount, limitAmount, ok := meteoraDammSwapAmountInfo(event, swapEvent.Params); ok {
}, offset, nil swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
}
return []Swap{swap}, offset, nil
} }

View File

@@ -1,12 +1,33 @@
package pump_parser package pump_parser
import ( import (
"encoding/binary"
"fmt" "fmt"
"github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal" "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) { func orcaWhirPoolParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(orcaProgramID) { 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]) return nil, increaseOffset(offset), fmt.Errorf("orcawhirpoolprogram instruction not found, offset, %d, %d", offset[0], offset[1])
@@ -242,10 +263,10 @@ func orcaWhirPoolLiquidityParser(tx *Tx, instruction Instruction, innerInstructi
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) //return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) { if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
baseAmount = decimal.NewFromInt(int64(amount)) baseAmount = decimal.NewFromUint64(amount)
baseFound = true baseFound = true
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) { } else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
quoteAmount = decimal.NewFromInt(int64(amount)) quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true quoteFound = true
} }
if baseFound && quoteFound { if baseFound && quoteFound {
@@ -260,7 +281,7 @@ func orcaWhirPoolLiquidityParser(tx *Tx, instruction Instruction, innerInstructi
return nil, increaseOffset(offset), InstructionIgnoredError return nil, increaseOffset(offset), InstructionIgnoredError
} }
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) { if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
instructionName += "_on_side" instructionName += "_one_side"
} }
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) { if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero") return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
@@ -349,10 +370,10 @@ func orcaWhirPoolLiquidityV2Parser(tx *Tx, instruction Instruction, innerInstruc
continue continue
} }
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) { if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
baseAmount = decimal.NewFromInt(int64(amount)) baseAmount = decimal.NewFromUint64(amount)
baseFound = true baseFound = true
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) { } else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
quoteAmount = decimal.NewFromInt(int64(amount)) quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true quoteFound = true
} }
if baseFound && quoteFound { if baseFound && quoteFound {
@@ -367,7 +388,7 @@ func orcaWhirPoolLiquidityV2Parser(tx *Tx, instruction Instruction, innerInstruc
return nil, offset, InstructionIgnoredError return nil, offset, InstructionIgnoredError
} }
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) { if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
instructionName += "_on_side" instructionName += "_one_side"
} }
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) { if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero") return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
@@ -454,10 +475,10 @@ func orcaWhirPoolCollectFeeParser(tx *Tx, instruction Instruction, innerInstruct
//return nil, increaseOffset(offset), fmt.Errorf("orca whirpool parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) //return nil, increaseOffset(offset), fmt.Errorf("orca whirpool parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) { if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
baseAmount = decimal.NewFromInt(int64(amount)) baseAmount = decimal.NewFromUint64(amount)
baseFound = true baseFound = true
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) { } else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
quoteAmount = decimal.NewFromInt(int64(amount)) quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true quoteFound = true
} }
if (baseFound && quoteFound) || i >= 6 { if (baseFound && quoteFound) || i >= 6 {
@@ -472,7 +493,7 @@ func orcaWhirPoolCollectFeeParser(tx *Tx, instruction Instruction, innerInstruct
return nil, offset, InstructionIgnoredError return nil, offset, InstructionIgnoredError
} }
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) { if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
instructionName += "_on_side" instructionName += "_one_side"
} }
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) { if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero") return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
@@ -556,10 +577,10 @@ func orcaWhirPoolCollectFeeV2Parser(tx *Tx, instruction Instruction, innerInstru
//return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) //return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) { if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
baseAmount = decimal.NewFromInt(int64(amount)) baseAmount = decimal.NewFromUint64(amount)
baseFound = true baseFound = true
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) { } else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
quoteAmount = decimal.NewFromInt(int64(amount)) quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true quoteFound = true
} }
if (baseFound && quoteFound) || i >= 6 { if (baseFound && quoteFound) || i >= 6 {
@@ -574,7 +595,7 @@ func orcaWhirPoolCollectFeeV2Parser(tx *Tx, instruction Instruction, innerInstru
return nil, offset, InstructionIgnoredError return nil, offset, InstructionIgnoredError
} }
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) { if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
instructionName += "_on_side" instructionName += "_one_side"
} }
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) { if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero") return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
@@ -658,10 +679,10 @@ func orcaWhirPoolCollectProtocolFeeV2Parser(tx *Tx, instruction Instruction, inn
// return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) // return nil, increaseOffset(offset), fmt.Errorf("meta Bonding Curve initial parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) { if !baseFound && (from.Equals(tx.rawTx.accountList[vault0]) || to.Equals(tx.rawTx.accountList[vault0])) {
baseAmount = decimal.NewFromInt(int64(amount)) baseAmount = decimal.NewFromUint64(amount)
baseFound = true baseFound = true
} else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) { } else if !quoteFound && (from.Equals(tx.rawTx.accountList[vault1]) || to.Equals(tx.rawTx.accountList[vault1])) {
quoteAmount = decimal.NewFromInt(int64(amount)) quoteAmount = decimal.NewFromUint64(amount)
quoteFound = true quoteFound = true
} }
if (baseFound && quoteFound) || i >= 6 { if (baseFound && quoteFound) || i >= 6 {
@@ -676,7 +697,7 @@ func orcaWhirPoolCollectProtocolFeeV2Parser(tx *Tx, instruction Instruction, inn
return nil, offset, InstructionIgnoredError return nil, offset, InstructionIgnoredError
} }
if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) { if baseAmount.Equal(decimal.Zero) || quoteAmount.Equal(decimal.Zero) {
instructionName += "_on_side" instructionName += "_one_side"
} }
if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) { if (baseTokenBalance == nil && !baseAmount.Equal(decimal.Zero)) || (quoteTokenBalance == nil && !quoteAmount.Equal(decimal.Zero)) {
return nil, offset, fmt.Errorf("token balance is nil but amount is not zero") return nil, offset, fmt.Errorf("token balance is nil but amount is not zero")
@@ -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] 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]] user := tx.rawTx.accountList[instruction.Accounts[1]]
pool := tx.rawTx.accountList[instruction.Accounts[2]] pool := tx.rawTx.accountList[instruction.Accounts[2]]
@@ -755,7 +784,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) { if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
baseAmount = decimal.NewFromInt(int64(amount)) baseAmount = decimal.NewFromUint64(amount)
if from.Equals(vault0Account) && to.Equals(token0Account) { if from.Equals(vault0Account) && to.Equals(token0Account) {
event = "buy" event = "buy"
} else if from.Equals(token0Account) && to.Equals(vault0Account) { } else if from.Equals(token0Account) && to.Equals(vault0Account) {
@@ -763,7 +792,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
} }
baseFound = true baseFound = true
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) { } else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
quoteAmount = decimal.NewFromInt(int64(amount)) quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(vault1Account) && to.Equals(token1Account) { if from.Equals(vault1Account) && to.Equals(token1Account) {
event = "sell" event = "sell"
} else if from.Equals(token1Account) && to.Equals(vault1Account) { } else if from.Equals(token1Account) && to.Equals(vault1Account) {
@@ -781,27 +810,28 @@ 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 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,
Program: SolProgramOrcaWhirPool, Event: event,
Event: event, Pool: pool,
Pool: pool, BaseMint: baseTokenBalance.MintAccount,
BaseMint: baseTokenBalance.MintAccount, QuoteMint: quoteTokenBalance.MintAccount,
QuoteMint: quoteTokenBalance.MintAccount, BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount, QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), BaseAmount: baseAmount,
BaseAmount: baseAmount, QuoteAmount: quoteAmount,
QuoteAmount: quoteAmount, BaseReserve: baseReserve,
BaseReserve: baseReserve, QuoteReserve: quoteReserve,
QuoteReserve: quoteReserve, UserBaseBalance: userBase,
UserBaseBalance: userBase, UserQuoteBalance: userQuote,
UserQuoteBalance: userQuote, User: user,
User: user, EntryContract: entryContract,
EntryContract: entryContract, }
}, swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
}, offset, nil
return []Swap{swap}, offset, nil
} }
func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { 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] 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]] user := tx.rawTx.accountList[instruction.Accounts[3]]
pool := tx.rawTx.accountList[instruction.Accounts[4]] pool := tx.rawTx.accountList[instruction.Accounts[4]]
@@ -856,7 +894,7 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swapv2 parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("orca whirpool swapv2 parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) { if !baseFound && (from.Equals(vault0Account) || to.Equals(vault0Account)) {
baseAmount = decimal.NewFromInt(int64(amount)) baseAmount = decimal.NewFromUint64(amount)
if from.Equals(vault0Account) && to.Equals(token0Account) { if from.Equals(vault0Account) && to.Equals(token0Account) {
event = "buy" event = "buy"
} else if from.Equals(token0Account) && to.Equals(vault0Account) { } else if from.Equals(token0Account) && to.Equals(vault0Account) {
@@ -864,7 +902,7 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
} }
baseFound = true baseFound = true
} else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) { } else if !quoteFound && (from.Equals(vault1Account) || to.Equals(vault1Account)) {
quoteAmount = decimal.NewFromInt(int64(amount)) quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(vault1Account) && to.Equals(token1Account) { if from.Equals(vault1Account) && to.Equals(token1Account) {
event = "sell" event = "sell"
} else if from.Equals(token1Account) && to.Equals(vault1Account) { } else if from.Equals(token1Account) && to.Equals(vault1Account) {
@@ -883,27 +921,28 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
} }
offset[1] += uint(skipOffset + 1) offset[1] += uint(skipOffset + 1)
return []Swap{ swap := Swap{
{ Program: SolProgramOrcaWhirPool,
Program: SolProgramOrcaWhirPool, Event: event,
Event: event, Pool: pool,
Pool: pool, BaseMint: baseTokenBalance.MintAccount,
BaseMint: baseTokenBalance.MintAccount, QuoteMint: quoteTokenBalance.MintAccount,
QuoteMint: quoteTokenBalance.MintAccount, BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount, QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), BaseAmount: baseAmount,
BaseAmount: baseAmount, QuoteAmount: quoteAmount,
QuoteAmount: quoteAmount, BaseReserve: baseReserve,
BaseReserve: baseReserve, QuoteReserve: quoteReserve,
QuoteReserve: quoteReserve, UserBaseBalance: userBase,
UserBaseBalance: userBase, UserQuoteBalance: userQuote,
UserQuoteBalance: userQuote, User: user,
User: user, EntryContract: entryContract,
EntryContract: entryContract, }
}, swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
}, offset, nil
return []Swap{swap}, offset, nil
} }
func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { 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] 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]] user := tx.rawTx.accountList[instruction.Accounts[1]]
pool1 := tx.rawTx.accountList[instruction.Accounts[2]] pool1 := tx.rawTx.accountList[instruction.Accounts[2]]
@@ -964,7 +1011,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) { if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
baseAmount = decimal.NewFromInt(int64(amount)) baseAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) { if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
event = "buy" event = "buy"
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) { } else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
@@ -972,7 +1019,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
} }
baseFound = true baseFound = true
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) { } else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
quoteAmount = decimal.NewFromInt(int64(amount)) quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool1UserQuote]) { if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool1UserQuote]) {
event = "sell" event = "sell"
} else if from.Equals(tx.rawTx.accountList[pool1UserQuote]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) { } else if from.Equals(tx.rawTx.accountList[pool1UserQuote]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
@@ -1040,7 +1087,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) { if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
baseAmount = decimal.NewFromInt(int64(amount)) baseAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool2UserBase]) { if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool2UserBase]) {
event = "buy" event = "buy"
} else if from.Equals(tx.rawTx.accountList[pool2UserBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) { } else if from.Equals(tx.rawTx.accountList[pool2UserBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
@@ -1048,7 +1095,7 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
} }
baseFound = true baseFound = true
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) { } else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
quoteAmount = decimal.NewFromInt(int64(amount)) quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) { if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
event = "sell" event = "sell"
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) { } else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
@@ -1082,6 +1129,30 @@ func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstruct
EntryContract: entryContract, 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,
)
swaps[0].SlippageBps = decimal.Zero
return swaps, offset, nil return swaps, offset, nil
} }
@@ -1091,6 +1162,14 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
} }
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] 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]] user := tx.rawTx.accountList[instruction.Accounts[14]]
pool1 := tx.rawTx.accountList[instruction.Accounts[0]] pool1 := tx.rawTx.accountList[instruction.Accounts[0]]
@@ -1142,7 +1221,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) { if from.Equals(tx.rawTx.accountList[pool1VaultBase]) || to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
baseAmount = decimal.NewFromInt(int64(amount)) baseAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) { if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool1UserBase]) {
event = "buy" event = "buy"
} else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) { } else if from.Equals(tx.rawTx.accountList[pool1UserBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
@@ -1150,7 +1229,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
} }
baseFound = true baseFound = true
} else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) { } else if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) || to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
quoteAmount = decimal.NewFromInt(int64(amount)) quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) { if from.Equals(tx.rawTx.accountList[pool1VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
event = "sell" event = "sell"
} else if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) { } else if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultQuote]) {
@@ -1216,7 +1295,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1]) return nil, increaseOffset(offset), fmt.Errorf("orca whirpool two hop swap parse token transfer error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) { if from.Equals(tx.rawTx.accountList[pool2VaultBase]) || to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
baseAmount = decimal.NewFromInt(int64(amount)) baseAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) { if from.Equals(tx.rawTx.accountList[pool2VaultBase]) && to.Equals(tx.rawTx.accountList[pool1VaultBase]) {
event = "buy" event = "buy"
} else if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) { } else if from.Equals(tx.rawTx.accountList[pool1VaultBase]) && to.Equals(tx.rawTx.accountList[pool2VaultBase]) {
@@ -1224,7 +1303,7 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
} }
baseFound = true baseFound = true
} else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) { } else if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) || to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
quoteAmount = decimal.NewFromInt(int64(amount)) quoteAmount = decimal.NewFromUint64(amount)
if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) { if from.Equals(tx.rawTx.accountList[pool2VaultQuote]) && to.Equals(tx.rawTx.accountList[pool2UserQuote]) {
event = "sell" event = "sell"
} else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) { } else if from.Equals(tx.rawTx.accountList[pool2UserQuote]) && to.Equals(tx.rawTx.accountList[pool2VaultQuote]) {
@@ -1258,5 +1337,29 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
EntryContract: entryContract, 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,
)
swaps[0].SlippageBps = decimal.Zero
return swaps, offset, nil return swaps, offset, nil
} }

73
pump.go
View File

@@ -218,6 +218,44 @@ type PumpTradeArgs struct {
Amount2 uint64 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 pumpCompleteMatchesTradeEvent(completeEvent CompleteEvent, tradeEvent PumpTradeEvent, bondingCurve solana.PublicKey) bool {
if completeEvent.Mint != tradeEvent.Mint {
return false
}
if completeEvent.User != tradeEvent.User {
return false
}
if completeEvent.BondingCurve != bondingCurve {
return false
}
return true
}
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) { func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if tx.Err == nil || tx.Err.UnKnown != "" { 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]) return nil, increaseOffset(offset), fmt.Errorf("tx pump failed but error is nil, offset, %d, %d", offset[0], offset[1])
@@ -315,6 +353,10 @@ func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions
EntryContract: entryContract, EntryContract: entryContract,
}, },
} }
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
normalizePumpQuoteSideMint(&swaps[0])
}
return swaps, offset, nil return swaps, offset, nil
} }
@@ -337,6 +379,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
completeEvent CompleteEvent completeEvent CompleteEvent
completed bool completed bool
newoffset [2]uint newoffset [2]uint
tradeFound bool
) )
var prefixLen = offset[1] var prefixLen = offset[1]
@@ -365,6 +408,9 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
} }
if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) { if innerInstr.ProgramIDIndex == programIndex && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) {
if bytes.Equal(innerInstr.Data[8:16], pumpTradeEventDiscriminator[8:16]) { if bytes.Equal(innerInstr.Data[8:16], pumpTradeEventDiscriminator[8:16]) {
if tradeFound {
break
}
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent) err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&tradeEvent)
if offset[1] == 0 { if offset[1] == 0 {
newoffset = [2]uint{offset[0] + 1, offset[1]} newoffset = [2]uint{offset[0] + 1, offset[1]}
@@ -374,19 +420,31 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
if err != nil { if err != nil {
return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1]) return nil, newoffset, fmt.Errorf("pump buy event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
} }
expectedIsBuy := !bytes.Equal(instruction.Data[:8], pumpSellDiscriminator[:])
if tradeEvent.IsBuy != expectedIsBuy {
tradeEvent = PumpTradeEvent{}
continue
}
tradeFound = true
if !tradeEvent.IsBuy { if !tradeEvent.IsBuy {
break break
} }
} else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) { } else if bytes.Equal(innerInstr.Data[8:16], pumpCompleteEventDiscriminator[:]) {
if !tradeFound {
continue
}
err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&completeEvent) err = agbinary.NewBorshDecoder(innerInstr.Data[16:]).Decode(&completeEvent)
if err != nil {
return nil, increaseOffset(offset), fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, result.accountList[instruction.Accounts[3]]) {
break
}
if offset[1] == 0 { if offset[1] == 0 {
newoffset = [2]uint{offset[0] + 1, offset[1]} newoffset = [2]uint{offset[0] + 1, offset[1]}
} else { } else {
newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1} newoffset = [2]uint{offset[0], prefixLen + uint(innerIndex) + 1}
} }
if err != nil {
return nil, newoffset, fmt.Errorf("pump completeEvent event decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
completed = true completed = true
break break
} }
@@ -399,6 +457,11 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
offset = [2]uint{newoffset[0], newoffset[1]} offset = [2]uint{newoffset[0], newoffset[1]}
var args PumpTradeArgs
if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err != nil {
return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1])
}
event := "" event := ""
baseTokenProgram := solana.TokenProgramID baseTokenProgram := solana.TokenProgramID
if tradeEvent.IsBuy { if tradeEvent.IsBuy {
@@ -466,6 +529,10 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
Cashback: isCashbackCoin, Cashback: isCashbackCoin,
}, },
} }
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
normalizePumpQuoteSideMint(&swaps[0])
}
if completed { if completed {
swaps = append(swaps, Swap{ swaps = append(swaps, Swap{
Program: SolProgramPump, Program: SolProgramPump,

View File

@@ -11,6 +11,31 @@ import (
"github.com/mr-tron/base58" "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) { func TestTradeEvent(t *testing.T) {
hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e" hexData := "e445a52e51cb9a1dbddb7fd34ee661ee051d1834b36cc6f04cc5bd998d53ab2a566a0ca2415bcfad5f9ed6941a851d3f84ecb200000000006c267d17170000000190d2c525ef0ea205f4b4abfdb6eaaf37fcb5a1b1dec2e2689448eecab6ba93b6c922246900000000c314f11a0d000000be71bf9e22080200c368cd1e06000000bed9ac52910901004ac2f8d0dd5cbc97e3289c197cb5062a54f3d956b9ce6e5115f96567aa5cb3e65f000000000000002c6a010000000000c9e17c171227a50a5b62e3a4a3f8ff4fafe0bca9c332bdf7f32eedbc4229604d1e000000000000005f72000000000000010000000000000000000000000000000000000000000000000000000000000000100000006275795f65786163745f736f6c5f696e"
d, err := hex.DecodeString(hexData) d, err := hex.DecodeString(hexData)
@@ -18,13 +43,21 @@ func TestTradeEvent(t *testing.T) {
t.Errorf("Failed to decode base64 data: %v", err) t.Errorf("Failed to decode base64 data: %v", err)
} }
var tradeEvent PumpTradeEvent var tradeEvent legacyPumpTradeEvent
err = agbinary.NewBorshDecoder(d[16:]).Decode(&tradeEvent) err = agbinary.NewBorshDecoder(d[16:]).Decode(&tradeEvent)
if err != nil { 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) t.Logf("Trade Event: %+v", tradeEvent)
xx, err := base58.Decode("3Bxs48EzTZB4tzRd") xx, err := base58.Decode("3Bxs48EzTZB4tzRd")
@@ -43,3 +76,27 @@ func TestCal(t *testing.T) {
fmt.Println(solana.MustPublicKeyFromBase58("BM9CcyErJcu2mjrFvUsRRrD3snGeHDDVirJLvL6EjvMN").IsOnCurve()) fmt.Println(solana.MustPublicKeyFromBase58("BM9CcyErJcu2mjrFvUsRRrD3snGeHDDVirJLvL6EjvMN").IsOnCurve())
} }
func TestPumpCompleteMatchesTradeEvent(t *testing.T) {
mint := solana.MustPublicKeyFromBase58("8GNGkNnfBuoTP3QRnmdNzSYuuE15M8tvcNvxNsV4pump")
user := solana.MustPublicKeyFromBase58("DS95KxqUCCjwQaXhD7fhKatXbivwWDNrJdNV5ZcubGdz")
bondingCurve := solana.MustPublicKeyFromBase58("Gz5EX3X7kUDS48baijJKubQDKy3BBKpnMJQ3f3W1e9jA")
tradeEvent := PumpTradeEvent{
Mint: mint,
User: user,
}
completeEvent := CompleteEvent{
Mint: mint,
User: user,
BondingCurve: bondingCurve,
}
if !pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) {
t.Fatal("pumpCompleteMatchesTradeEvent() = false, want true")
}
completeEvent.User = solana.MustPublicKeyFromBase58("3g89wLRwJ5P22fkCdPJBAP7iiYAo6yY96geQvMYj6tYm")
if pumpCompleteMatchesTradeEvent(completeEvent, tradeEvent, bondingCurve) {
t.Fatal("pumpCompleteMatchesTradeEvent() = true for mismatched user")
}
}

View File

@@ -261,6 +261,19 @@ type PumpSwapArgs struct {
Amount2 uint64 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) { func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if tx.Err == nil || tx.Err.UnKnown != "" { 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]) return nil, increaseOffset(offset), fmt.Errorf("tx pump amm sell failed but error is nil, offset, %d, %d", offset[0], offset[1])
@@ -361,28 +374,30 @@ func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions Inn
} }
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7]) baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8]) quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
return []Swap{ swap := Swap{
{ Program: SolProgramPumpAMM,
Program: SolProgramPumpAMM, Event: event,
Event: event, Pool: tx.rawTx.accountList[instruction.Accounts[0]],
Pool: tx.rawTx.accountList[instruction.Accounts[0]], BaseMint: baseMint,
BaseMint: baseMint, QuoteMint: quoteMint,
QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram,
BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram,
QuoteTokenProgram: quoteTokenProgram, BaseMintDecimals: baseMintDecimals,
BaseMintDecimals: baseMintDecimals, QuoteMintDecimals: quoteMintDecimals,
QuoteMintDecimals: quoteMintDecimals, User: eventUser,
User: eventUser, BaseAmount: decimal.NewFromUint64(tokenAmount),
BaseAmount: decimal.NewFromUint64(tokenAmount), QuoteAmount: decimal.NewFromUint64(quoteAmount),
QuoteAmount: decimal.NewFromUint64(quoteAmount), BaseReserve: baseReserve,
BaseReserve: baseReserve, QuoteReserve: quoteReserve,
QuoteReserve: quoteReserve, Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), UserBaseBalance: userBase,
UserBaseBalance: userBase, UserQuoteBalance: userQuote,
UserQuoteBalance: userQuote, EntryContract: entryContract,
EntryContract: entryContract, }
}, if swapMode, fixedAmount, limitAmount, ok := pumpAmmSwapAmountInfoFromArgs(args); ok {
}, offset, nil 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) { func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -479,28 +494,30 @@ func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions In
} }
baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7]) baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7])
quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8]) quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8])
return []Swap{ swap := Swap{
{ Program: SolProgramPumpAMM,
Program: SolProgramPumpAMM, Event: event,
Event: event, Pool: tx.rawTx.accountList[instruction.Accounts[0]],
Pool: tx.rawTx.accountList[instruction.Accounts[0]], BaseMint: baseMint,
BaseMint: baseMint, QuoteMint: quoteMint,
QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram,
BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram,
QuoteTokenProgram: quoteTokenProgram, BaseMintDecimals: baseMintDecimals,
BaseMintDecimals: baseMintDecimals, QuoteMintDecimals: quoteMintDecimals,
QuoteMintDecimals: quoteMintDecimals, User: eventUser,
User: eventUser, BaseAmount: decimal.NewFromUint64(tokenAmount),
BaseAmount: decimal.NewFromUint64(tokenAmount), QuoteAmount: decimal.NewFromUint64(quoteAmount),
QuoteAmount: decimal.NewFromUint64(quoteAmount), BaseReserve: baseReserve,
BaseReserve: baseReserve, QuoteReserve: quoteReserve,
QuoteReserve: quoteReserve, Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), UserBaseBalance: userBase,
UserBaseBalance: userBase, UserQuoteBalance: userQuote,
UserQuoteBalance: userQuote, EntryContract: entryContract,
EntryContract: entryContract, }
}, if swapMode, fixedAmount, limitAmount, ok := pumpAmmSwapAmountInfoFromArgs(args); ok {
}, offset, nil 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) { func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -599,30 +616,42 @@ func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstru
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance)) userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
} }
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0 isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
return []Swap{ swap := Swap{
{ Program: SolProgramPumpAMM,
Program: SolProgramPumpAMM, Event: "buy",
Event: "buy", Pool: event.Pool,
Pool: event.Pool, BaseMint: baseMint,
BaseMint: baseMint, QuoteMint: quoteMint,
QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram,
BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram,
QuoteTokenProgram: quoteTokenProgram, Creator: event.CoinCreator,
Creator: event.CoinCreator, BaseMintDecimals: baseMintDecimals,
BaseMintDecimals: baseMintDecimals, QuoteMintDecimals: quoteMintDecimals,
QuoteMintDecimals: quoteMintDecimals, User: eventUser,
User: eventUser, BaseAmount: decimal.NewFromUint64(event.BaseAmountOut),
BaseAmount: decimal.NewFromUint64(event.BaseAmountOut), QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountIn),
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountIn), BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut),
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserve - event.BaseAmountOut), QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn),
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserve + event.QuoteAmountIn), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), Cashback: isCashbackCoin,
Cashback: isCashbackCoin, UserBaseBalance: userBase,
UserBaseBalance: userBase, UserQuoteBalance: userQuote,
UserQuoteBalance: userQuote, EntryContract: entryContract,
EntryContract: entryContract, }
}, if bytes.Equal(instruction.Data[:8], pumpAmmBuyV2Discriminator[:]) {
}, offset, nil 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) { func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
@@ -722,30 +751,34 @@ func ammSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr
userQuote = userQuote.Add(decimal.NewFromUint64(userBalance)) userQuote = userQuote.Add(decimal.NewFromUint64(userBalance))
} }
isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0 isCashbackCoin := event.CashbackFeeBasisPoints > 0 || event.Cashback > 0
return []Swap{ swap := Swap{
{ Program: SolProgramPumpAMM,
Program: SolProgramPumpAMM, Event: "sell",
Event: "sell", Pool: event.Pool,
Pool: event.Pool, BaseMint: baseMint,
BaseMint: baseMint, QuoteMint: quoteMint,
QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram,
BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram,
QuoteTokenProgram: quoteTokenProgram, Creator: event.CoinCreator,
Creator: event.CoinCreator, BaseMintDecimals: baseMintDecimals,
BaseMintDecimals: baseMintDecimals, QuoteMintDecimals: quoteMintDecimals,
QuoteMintDecimals: quoteMintDecimals, User: eventUser,
User: eventUser, BaseAmount: decimal.NewFromUint64(event.BaseAmountIn),
BaseAmount: decimal.NewFromUint64(event.BaseAmountIn), QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountOut),
QuoteAmount: decimal.NewFromUint64(event.UserQuoteAmountOut), BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn),
BaseReserve: decimal.NewFromUint64(event.PoolBaseTokenReserves + event.BaseAmountIn), QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut),
QuoteReserve: decimal.NewFromUint64(event.PoolQuoteTokenReserves - event.QuoteAmountOut), Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]),
Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), Cashback: isCashbackCoin,
Cashback: isCashbackCoin, UserBaseBalance: userBase,
UserBaseBalance: userBase, UserQuoteBalance: userQuote,
UserQuoteBalance: userQuote, EntryContract: entryContract,
EntryContract: entryContract, }
}, swap.SetSwapAmountInfo(
}, offset, nil 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) { func depositParse(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {

View File

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

View File

@@ -1,12 +1,27 @@
package pump_parser package pump_parser
import ( import (
"encoding/binary"
"fmt" "fmt"
"github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal" "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) { func raydiumClmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumClmmProgramID) { 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]) 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 userTokenOutAccount int
) )
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] 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 { if discriminator == raydiumClmmSwapDiscriminator {
accountMin = 9 accountMin = 9
pool = tx.rawTx.accountList[instruction.Accounts[2]] pool = tx.rawTx.accountList[instruction.Accounts[2]]
@@ -350,26 +369,26 @@ func raydiumClmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
offset[1] += 2 offset[1] += 2
return []Swap{ swap := Swap{
{ Program: SolProgramRaydiumCLMM,
Program: SolProgramRaydiumCLMM, Event: "sell",
Event: "sell", Pool: pool,
Pool: pool, BaseMint: baseMint,
BaseMint: baseMint, QuoteMint: quoteMint,
QuoteMint: quoteMint, BaseTokenProgram: baseTokenProgram,
BaseTokenProgram: baseTokenProgram, QuoteTokenProgram: quoteTokenProgram,
QuoteTokenProgram: quoteTokenProgram, BaseMintDecimals: baseMintDecimals,
BaseMintDecimals: baseMintDecimals, QuoteMintDecimals: quoteMintDecimals,
QuoteMintDecimals: quoteMintDecimals, User: tx.rawTx.accountList[instruction.Accounts[0]],
User: tx.rawTx.accountList[instruction.Accounts[0]], BaseAmount: baseAmount,
BaseAmount: baseAmount, QuoteAmount: quoteAmount,
QuoteAmount: quoteAmount, BaseReserve: baseReserve,
BaseReserve: baseReserve, QuoteReserve: quoteReserve,
QuoteReserve: quoteReserve, UserBaseBalance: userBase,
UserBaseBalance: userBase, UserQuoteBalance: userQuote,
UserQuoteBalance: userQuote, EntryContract: entryContract,
EntryContract: entryContract, }
}, swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
}, offset, nil return []Swap{swap}, offset, nil
} }

View File

@@ -4,9 +4,20 @@ import (
"bytes" "bytes"
"fmt" "fmt"
agbinary "github.com/gagliardetto/binary"
"github.com/shopspring/decimal" "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) { func raydiumCPmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumCPmmProgramID) { 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]) 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] 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]] market := tx.rawTx.accountList[instruction.Accounts[3]]
// Get token accounts from instruction // Get token accounts from instruction
tokenIn := tx.rawTx.accountList[instruction.Accounts[4]] tokenIn := tx.rawTx.accountList[instruction.Accounts[4]]
@@ -384,25 +419,26 @@ func raydiumCPmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In
return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions") return nil, increaseOffset(offset), fmt.Errorf("failed to find token transfer in inner instructions")
} }
offset[1] += 2 offset[1] += 2
return []Swap{ swap := Swap{
{ Program: SolProgramRaydiumCPMM,
Program: SolProgramRaydiumCPMM, Event: "sell",
Event: "sell", Pool: market,
Pool: market, BaseMint: inputTokenMint,
BaseMint: inputTokenMint, QuoteMint: outputTokenMint,
QuoteMint: outputTokenMint, BaseTokenProgram: baseTokenBalance.ProgramIDAccount,
BaseTokenProgram: baseTokenBalance.ProgramIDAccount, QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenBalance.ProgramIDAccount, BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals),
BaseMintDecimals: uint8(baseTokenBalance.UITokenAmount.Decimals), QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenBalance.UITokenAmount.Decimals), BaseReserve: baseReserve,
BaseReserve: baseReserve, QuoteReserve: quoteReserve,
QuoteReserve: quoteReserve, BaseAmount: baseAmount,
BaseAmount: baseAmount, QuoteAmount: quoteAmount,
QuoteAmount: quoteAmount, User: user,
User: user, UserBaseBalance: userBase,
UserBaseBalance: userBase, UserQuoteBalance: userQuote,
UserQuoteBalance: userQuote, EntryContract: entryContract,
EntryContract: entryContract, }
}, swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
}, offset, nil
return []Swap{swap}, offset, nil
} }

View File

@@ -367,6 +367,11 @@ type RaydiumLaunchLabSwapEvent struct {
} }
type raydiumLaunchLabSwapArgs struct {
Amount uint64
OtherAmountThreshold uint64
}
func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
platformConfig := tx.rawTx.accountList[instruction.Accounts[3]] platformConfig := tx.rawTx.accountList[instruction.Accounts[3]]
var programName string var programName string
@@ -375,6 +380,26 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
} else { } else {
programName = SolProgramRaydiumLaunchLab 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] var entryContract solana.PublicKey = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex]
user := tx.rawTx.accountList[instruction.Accounts[0]] user := tx.rawTx.accountList[instruction.Accounts[0]]
pool := tx.rawTx.accountList[instruction.Accounts[4]] pool := tx.rawTx.accountList[instruction.Accounts[4]]
@@ -447,7 +472,7 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
userBase := getAccountBalanceAfterTx(tx.rawTx, userBaseIdx) userBase := getAccountBalanceAfterTx(tx.rawTx, userBaseIdx)
userQuote := getAccountBalanceAfterTx(tx.rawTx, userQuoteIdx) userQuote := getAccountBalanceAfterTx(tx.rawTx, userQuoteIdx)
return []Swap{{ swap := Swap{
Program: programName, Program: programName,
Event: event, Event: event,
Pool: pool, Pool: pool,
@@ -466,5 +491,8 @@ func raydiumLaunchLabSwapParser(tx *Tx, instruction Instruction, innerInstructio
UserBaseBalance: userBase, UserBaseBalance: userBase,
UserQuoteBalance: userQuote, UserQuoteBalance: userQuote,
EntryContract: entryContract, EntryContract: entryContract,
}}, offset, nil }
swap.SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
return []Swap{swap}, offset, nil
} }

View File

@@ -1,11 +1,26 @@
package pump_parser package pump_parser
import ( import (
"encoding/binary"
"fmt" "fmt"
"github.com/shopspring/decimal" "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) { func raydiumV4Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(raydiumV4Program) { 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]) 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] vaultQuoteIdx = instruction.Accounts[6]
} }
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] 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]] ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
@@ -376,37 +395,44 @@ func raydiumv4SwapParser(tx *Tx, instruction Instruction, innerInstructions Inne
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount) baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount) quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
return []Swap{ swap := Swap{
{ Program: SolProgramRaydiumV4,
Program: SolProgramRaydiumV4, Event: event,
Event: event, Pool: ammAccount,
Pool: ammAccount, BaseMint: baseTokenbalance.MintAccount,
BaseMint: baseTokenbalance.MintAccount, QuoteMint: quoteTokenbalance.MintAccount,
QuoteMint: quoteTokenbalance.MintAccount, BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
BaseTokenProgram: baseTokenbalance.ProgramIDAccount, QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount, BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals), QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals), User: user,
User: user, BaseAmount: baseAmount,
BaseAmount: baseAmount, QuoteAmount: quoteAmount,
QuoteAmount: quoteAmount, BaseReserve: baseReserve,
BaseReserve: baseReserve, QuoteReserve: quoteReserve,
QuoteReserve: quoteReserve, Mayhem: false,
Mayhem: false, UserBaseBalance: userBase,
UserBaseBalance: userBase, UserQuoteBalance: userQuote,
UserQuoteBalance: userQuote, EntryContract: entryContract,
EntryContract: entryContract, }
}, swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
}, offset, nil return []Swap{swap}, offset, nil
} }
func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) {
accountsLen := len(instruction.Accounts) 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]) 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] 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]] ammAccount := tx.rawTx.accountList[instruction.Accounts[1]]
user := tx.rawTx.accountList[instruction.Accounts[7]] user := tx.rawTx.accountList[instruction.Accounts[7]]
userSourceTokenAccount := tx.rawTx.accountList[instruction.Accounts[5]] userSourceTokenAccount := tx.rawTx.accountList[instruction.Accounts[5]]
@@ -472,26 +498,26 @@ func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions In
baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount) baseReserve, _ := decimal.NewFromString(baseTokenbalance.UITokenAmount.Amount)
quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount) quoteReserve, _ := decimal.NewFromString(quoteTokenbalance.UITokenAmount.Amount)
return []Swap{ swap := Swap{
{ Program: SolProgramRaydiumV4,
Program: SolProgramRaydiumV4, Event: event,
Event: event, Pool: ammAccount,
Pool: ammAccount, BaseMint: baseTokenbalance.MintAccount,
BaseMint: baseTokenbalance.MintAccount, QuoteMint: quoteTokenbalance.MintAccount,
QuoteMint: quoteTokenbalance.MintAccount, BaseTokenProgram: baseTokenbalance.ProgramIDAccount,
BaseTokenProgram: baseTokenbalance.ProgramIDAccount, QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount,
QuoteTokenProgram: quoteTokenbalance.ProgramIDAccount, BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals),
BaseMintDecimals: uint8(baseTokenbalance.UITokenAmount.Decimals), QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals),
QuoteMintDecimals: uint8(quoteTokenbalance.UITokenAmount.Decimals), User: user,
User: user, BaseAmount: baseAmount,
BaseAmount: baseAmount, QuoteAmount: quoteAmount,
QuoteAmount: quoteAmount, BaseReserve: baseReserve,
BaseReserve: baseReserve, QuoteReserve: quoteReserve,
QuoteReserve: quoteReserve, Mayhem: false,
Mayhem: false, UserBaseBalance: userBase,
UserBaseBalance: userBase, UserQuoteBalance: userQuote,
UserQuoteBalance: userQuote, EntryContract: entryContract,
EntryContract: entryContract, }
}, swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amountSpecified), decimal.NewFromUint64(otherAmountThreshold))
}, offset, nil return []Swap{swap}, offset, nil
} }

144
raydiumv4_test.go Normal file
View File

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

230
swap_amounts.go Normal file
View File

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

387
swap_amounts_oracle_test.go Normal file
View File

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

206
swap_amounts_test.go Normal file
View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

35
tx.go
View File

@@ -31,6 +31,18 @@ type Swap struct {
BaseAmount decimal.Decimal BaseAmount decimal.Decimal
QuoteAmount 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 BaseReserve decimal.Decimal
QuoteReserve decimal.Decimal QuoteReserve decimal.Decimal
Mayhem bool Mayhem bool
@@ -48,23 +60,22 @@ type Swap struct {
AfterSOLBalance decimal.Decimal AfterSOLBalance decimal.Decimal
//For meteora dlmm //For meteora dlmm
ActiveBinId int32 ActiveBinId int32
StartBinId int32 StartBinId int32
EndBinId int32 EndBinId int32
RemoveBp int32 RemoveBp int32
BinChanges []DlmmBinLiquidityChange
PositionAccount solana.PublicKey PositionAccount solana.PublicKey
FeeAmount decimal.Decimal
FeeBps string
LpFeeAmount decimal.Decimal
FeeSide string
FeeMint solana.PublicKey
FeeTokenProgram solana.PublicKey
FeeMintDecimals uint8
ConsumeUnit uint64 ConsumeUnit uint64
} }
type DlmmBinLiquidityChange struct {
BinId int32
AmountX decimal.Decimal
AmountY decimal.Decimal
BpsToRemove uint16
}
type platformInfo struct { type platformInfo struct {
Platform string Platform string
PlatformFee decimal.Decimal PlatformFee decimal.Decimal

2206
tx_binary.go Normal file

File diff suppressed because it is too large Load Diff

146
tx_binary_realdata_test.go Normal file
View File

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

903
tx_binary_test.go Normal file
View File

@@ -0,0 +1,903 @@
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 TestTxBinaryCanonicalizesOnSideEventAlias(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: SolProgramOrcaWhirPool,
Event: "remove_liquidity_on_side",
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.Zero,
SwapMode: SwapModeExactIn,
FixedAmount: decimal.NewFromInt(10),
FixedAmountSide: SwapAmountSideBase,
FixedMint: mustPubKey("3wyAj7RtG72wM1Wv9DkYfL7RAx9X3Jx1sC6E6mN4jWeL"),
LimitAmountType: SwapLimitTypeMinOut,
LimitAmount: decimal.Zero,
LimitAmountSide: SwapAmountSideQuote,
ActualLimitAmount: decimal.Zero,
ActualLimitAmountSide: SwapAmountSideQuote,
BaseReserve: decimal.RequireFromString("123.4"),
QuoteReserve: decimal.RequireFromString("456.7"),
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].Event; got != TxEventRemoveLiquidityOneSide {
t.Fatalf("Event = %q, want %q", got, TxEventRemoveLiquidityOneSide)
}
}
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
}