From fe94888b1498486fff299ff17e93c5267a0dfc6d Mon Sep 17 00:00:00 2001 From: thloyi Date: Mon, 20 Apr 2026 12:31:30 +0800 Subject: [PATCH] fix slippage --- SLIPPAGE_MAPPING.md | 2 +- internal/example/cmd/main.go | 17 +++++++--- internal/example/yellowstone.go | 30 ++++++++++-------- meteoradamm.go | 14 +++------ swap_amounts.go | 33 ++++++++++++++----- swap_amounts_test.go | 56 +++++++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 36 deletions(-) diff --git a/SLIPPAGE_MAPPING.md b/SLIPPAGE_MAPPING.md index ad81ec2..50fe637 100644 --- a/SLIPPAGE_MAPPING.md +++ b/SLIPPAGE_MAPPING.md @@ -68,7 +68,7 @@ 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 +- 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: diff --git a/internal/example/cmd/main.go b/internal/example/cmd/main.go index 7f82dd4..eaa67e6 100644 --- a/internal/example/cmd/main.go +++ b/internal/example/cmd/main.go @@ -25,7 +25,7 @@ func main() { // laserstream-mainnet-slc.helius-rpc.com:80 ch := make(chan example.SubscriptionMessage, 1) - go example.RunLoopWithReConnect(context.Background(), "127.0.0.1:10001", parser.SolProgramPump, ch) + go example.RunLoopWithReConnect(context.Background(), "", "", parser.SolProgramPump, ch) // var tokenTxs = make(map[string]*types.Tx) // currentBlock := uint64(0) for msg := range ch { @@ -51,9 +51,18 @@ func main() { //} // 处理交易 - if len(ptx.Swaps) > 0 && (ptx.Swaps[0].Program == parser.SolProgramPump || ptx.Swaps[0].Program == parser.SolProgramPumpAMM) { - fmt.Printf("success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s \n", time.Now().Format("2006-01-02 15:04:05"), ptx.Swaps[0].Program, ptx.Swaps[0].Event, ptx.Block, ptx.GetTxHash(), - ptx.Swaps[0].BaseAmount.Div(decimal.NewFromInt(1e6)), ptx.Swaps[0].QuoteAmount.Div(decimal.NewFromInt(1e9))) + if len(ptx.Swaps) > 0 { + for _, swap := range ptx.Swaps { + if swap.SlippageBps.LessThan(decimal.Zero) || swap.SlippageBps.GreaterThan(decimal.NewFromInt(10000)) { + fmt.Printf("success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s \n", time.Now().Format("2006-01-02 15:04:05"), swap.Program, swap.Event, ptx.Block, ptx.GetTxHash(), + swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9))) + } + if swap.SlippageBps.Equal(decimal.Zero) && (swap.Event == "buy" || swap.Event == "sell") { + fmt.Printf("zero success tx : %s, program: %s, event: %s, block: %d, tx: %s, base: %s, quote: %s, fix: %s, limit: %s, \n", time.Now().Format("2006-01-02 15:04:05"), swap.Program, swap.Event, ptx.Block, ptx.GetTxHash(), + swap.BaseAmount.Div(decimal.NewFromInt(1e6)), swap.QuoteAmount.Div(decimal.NewFromInt(1e9)), swap.FixedAmount.String(), swap.LimitAmount.String()) + } + } + } // currentBlock = ptx.Block // diff --git a/internal/example/yellowstone.go b/internal/example/yellowstone.go index ea4eda7..c697aef 100644 --- a/internal/example/yellowstone.go +++ b/internal/example/yellowstone.go @@ -45,9 +45,11 @@ type Client struct { firstMessage bool handler Handler + + xToken string } -func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client { +func NewClientWithPumpSwap(endpoint string, xtoken string, ch chan SubscriptionMessage) *Client { var subscription pb.SubscribeRequest //var failed = true @@ -58,10 +60,10 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client Vote: &vote, } - subscription.Transactions["transactions_sub"].AccountInclude = []string{ - "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM - "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump - } + //subscription.Transactions["transactions_sub"].AccountInclude = []string{ + // "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM + // "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump + //} subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta) subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{} @@ -72,6 +74,7 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client lastReceiveTime: time.Now(), subStatus: false, subscription: &subscription, + xToken: xtoken, } c.handler = NewPumpHandler(func(tx *types.Tx) { c.sendTx(tx) @@ -112,12 +115,12 @@ func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Clien return c } -func RunLoopWithReConnect(ctx context.Context, endpoint, program string, ch chan SubscriptionMessage) { +func RunLoopWithReConnect(ctx context.Context, endpoint, token, program string, ch chan SubscriptionMessage) { var client *Client if program == types.SolProgramRaydiumLaunchLab { client = NewClientWithLaunchLab(endpoint, ch) } else { - client = NewClientWithPumpSwap(endpoint, ch) + client = NewClientWithPumpSwap(endpoint, token, ch) } for { select { @@ -206,12 +209,13 @@ func (c *Client) grpcSubscribe(ctx context.Context, conn *grpc.ClientConn) error log.Printf("Subscription request: %s", string(subscriptionJson)) // Set up the subscription request - //if *token != "" { - // md := metadata.New(map[string]string{"x-token": *token}) - // ctx = metadata.NewOutgoingContext(ctx, md) - //} - md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"}) - ctx = metadata.NewOutgoingContext(ctx, md) + if c.xToken != "" { + fmt.Println("xtoken", c.xToken) + md := metadata.New(map[string]string{"x-token": c.xToken}) + ctx = metadata.NewOutgoingContext(ctx, md) + } + //md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"}) + //ctx = metadata.NewOutgoingContext(ctx, md) stream, err := client.Subscribe(ctx) if err != nil { diff --git a/meteoradamm.go b/meteoradamm.go index 40445ee..2e217fc 100644 --- a/meteoradamm.go +++ b/meteoradamm.go @@ -193,6 +193,7 @@ func meteoraDammSwapAmountInfo(event string, params *struct { 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 } @@ -203,21 +204,14 @@ func meteoraDammSwapAmountInfo(event string, params *struct { // - 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 + // `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 - 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 + return swapMode, decimal.NewFromUint64(params.Amount0), decimal.NewFromUint64(params.Amount1), 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 diff --git a/swap_amounts.go b/swap_amounts.go index dca3cf3..c094a9f 100644 --- a/swap_amounts.go +++ b/swap_amounts.go @@ -9,6 +9,16 @@ import ( 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 @@ -141,29 +151,36 @@ func limitSwapAmountType(swapMode SwapMode) SwapLimitType { } func calculateLimitSlippageBps(limitType SwapLimitType, limitAmount, actualAmount decimal.Decimal) decimal.Decimal { + var value decimal.Decimal switch limitType { case SwapLimitTypeMinOut: if !actualAmount.IsPositive() { if !limitAmount.IsPositive() { - return maxSlippageBps + value = maxSlippageBps + break } - return maxSlippageBps.Neg() + value = maxSlippageBps.Neg() + break } if !limitAmount.IsPositive() { - return maxSlippageBps + value = maxSlippageBps + break } - return actualAmount.Sub(limitAmount).Mul(maxSlippageBps).Div(actualAmount) + value = actualAmount.Sub(limitAmount).Mul(maxSlippageBps).Div(actualAmount) case SwapLimitTypeMaxIn: if !limitAmount.IsPositive() { if !actualAmount.IsPositive() { - return maxSlippageBps + value = maxSlippageBps + break } - return maxSlippageBps.Neg() + value = maxSlippageBps.Neg() + break } - return limitAmount.Sub(actualAmount).Mul(maxSlippageBps).Div(limitAmount) + value = limitAmount.Sub(actualAmount).Mul(maxSlippageBps).Div(limitAmount) default: - return decimal.Zero + value = decimal.Zero } + return normalizeSlippageBps(value) } func (s *Swap) SetSwapAmountInfoDetailed( diff --git a/swap_amounts_test.go b/swap_amounts_test.go index 1a5382c..3082379 100644 --- a/swap_amounts_test.go +++ b/swap_amounts_test.go @@ -79,6 +79,38 @@ func TestSetSwapAmountInfoExactInZeroLimitUsesMaxSlippage(t *testing.T) { } } +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 @@ -116,6 +148,18 @@ func TestMeteoraDammSwapAmountInfo(t *testing.T) { 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, @@ -128,6 +172,18 @@ func TestMeteoraDammSwapAmountInfo(t *testing.T) { 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 {