swap amount input

This commit is contained in:
thloyi
2026-04-16 14:24:14 +08:00
parent ab0e87a48a
commit e761fd6f84
29 changed files with 1542 additions and 214 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: this usually indicates an incorrect parser-side mapping or inconsistent source data
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

@@ -92,6 +92,18 @@ func main() {
fmt.Printf(" quote_mint: %s (decimals=%d)\n", swap.QuoteMint, swap.QuoteMintDecimals) fmt.Printf(" quote_mint: %s (decimals=%d)\n", swap.QuoteMint, swap.QuoteMintDecimals)
fmt.Printf(" base_amount: %s\n", swap.BaseAmount.String()) fmt.Printf(" base_amount: %s\n", swap.BaseAmount.String())
fmt.Printf(" quote_amount: %s\n", swap.QuoteAmount.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 != "" { if !swap.FeeAmount.IsZero() || swap.FeeSide != "" {
fmt.Printf(" fee_amount: %s\n", swap.FeeAmount.String()) fmt.Printf(" fee_amount: %s\n", swap.FeeAmount.String())
fmt.Printf(" lp_fee_amount: %s\n", swap.LpFeeAmount.String()) fmt.Printf(" lp_fee_amount: %s\n", swap.LpFeeAmount.String())

View File

@@ -666,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
} }
@@ -861,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
} }

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,42 @@ 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) {
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
//
// The emitted event is normalized as token A <-> token B:
// - `sell` means A -> B, so A is the input side and B is the output side
// - `buy` means B -> A, so B is the input side and A is the output side
switch params.SwapMode {
case 0, 1: // ExactIn / PartialFill
swapMode = SwapModeExactIn
if event == TxEventSell {
return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), true
}
return swapMode, decimal.NewFromUint64(params.Amount1), decimal.NewFromUint64(params.Amount0), true
case 2: // ExactOut
swapMode = SwapModeExactOut
if event == TxEventSell {
return swapMode, decimal.NewFromUint64(params.Amount1), decimal.NewFromUint64(params.Amount0), true
}
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 +312,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])
@@ -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]]
@@ -781,8 +810,7 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
return nil, offset, fmt.Errorf("orca whirpool swap failed to find both base and quote token transfer in inner instructions") return 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,
@@ -800,8 +828,10 @@ func orcaWhirPoolSwapParser(tx *Tx, instruction Instruction, innerInstructions I
UserQuoteBalance: userQuote, UserQuoteBalance: userQuote,
User: user, User: user,
EntryContract: entryContract, EntryContract: entryContract,
}, }
}, offset, nil swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
} }
func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { 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]]
@@ -883,8 +921,7 @@ 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,
@@ -902,8 +939,10 @@ func orcaWhirPoolSwapV2Parser(tx *Tx, instruction Instruction, innerInstructions
UserQuoteBalance: userQuote, UserQuoteBalance: userQuote,
User: user, User: user,
EntryContract: entryContract, EntryContract: entryContract,
}, }
}, offset, nil swap.SetSwapAmountInfo(swapMode, decimal.NewFromUint64(amount), decimal.NewFromUint64(otherAmountThreshold))
return []Swap{swap}, offset, nil
} }
func orcaWhirPoolTwoHopSwapParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { 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]]
@@ -1082,6 +1129,29 @@ 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,
)
return swaps, offset, nil return swaps, offset, nil
} }
@@ -1091,6 +1161,14 @@ func orcaWhirPoolTwoHopSwapV2Parser(tx *Tx, instruction Instruction, innerInstru
} }
var entryContract = tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] 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]]
@@ -1258,5 +1336,28 @@ 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,
)
return swaps, offset, nil return swaps, offset, nil
} }

36
pump.go
View File

@@ -218,6 +218,31 @@ 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 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 +340,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
} }
@@ -466,6 +495,13 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns
Cashback: isCashbackCoin, Cashback: isCashbackCoin,
}, },
} }
var args PumpTradeArgs
if err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args); err == nil {
if swapMode, fixedAmount, limitAmount, ok := pumpTradeAmountInfoFromArgs(args); ok {
swaps[0].SetSwapAmountInfo(swapMode, fixedAmount, limitAmount)
normalizePumpQuoteSideMint(&swaps[0])
}
}
if completed { 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")

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

@@ -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,28 +395,28 @@ 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) {
@@ -406,6 +425,10 @@ func raydiumv4SwapV2Parser(tx *Tx, instruction Instruction, innerInstructions In
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 // 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 // may append extra readonly accounts (for example the Raydium program id) at
@@ -475,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
} }

View File

@@ -15,6 +15,14 @@ func transferInstructionData(amount uint64) solana.Base58 {
return solana.Base58(data) 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) { func TestRaydiumV4SwapV2ParserAllowsTrailingReadonlyAccounts(t *testing.T) {
t.Parallel() t.Parallel()
@@ -33,7 +41,7 @@ func TestRaydiumV4SwapV2ParserAllowsTrailingReadonlyAccounts(t *testing.T) {
swapInstruction := Instruction{ swapInstruction := Instruction{
Accounts: []int{0, 1, 2, 3, 4, 5, 6, 7, 8}, Accounts: []int{0, 1, 2, 3, 4, 5, 6, 7, 8},
ProgramIDIndex: 8, ProgramIDIndex: 8,
Data: solana.Base58([]byte{raydiumV4SwapBaseInV2Discriminator}), Data: raydiumV4SwapInstructionData(raydiumV4SwapBaseInV2Discriminator, 55, 42),
} }
innerInstructions := InnerInstructions{ innerInstructions := InnerInstructions{
Index: 0, Index: 0,

213
swap_amounts.go Normal file
View File

@@ -0,0 +1,213 @@
package pump_parser
import (
"encoding/json"
"github.com/gagliardetto/solana-go"
"github.com/shopspring/decimal"
)
var maxSlippageBps = decimal.NewFromInt(10000)
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 {
switch limitType {
case SwapLimitTypeMinOut:
if !actualAmount.IsPositive() {
if !limitAmount.IsPositive() {
return maxSlippageBps
}
return maxSlippageBps.Neg()
}
if !limitAmount.IsPositive() {
return maxSlippageBps
}
return actualAmount.Sub(limitAmount).Mul(maxSlippageBps).Div(actualAmount)
case SwapLimitTypeMaxIn:
if !limitAmount.IsPositive() {
if !actualAmount.IsPositive() {
return maxSlippageBps
}
return maxSlippageBps.Neg()
}
return limitAmount.Sub(actualAmount).Mul(maxSlippageBps).Div(limitAmount)
default:
return decimal.Zero
}
}
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)
}
}

150
swap_amounts_test.go Normal file
View File

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

12
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