From d9a214b4b411c75598c7341a56976b594e0dcef9 Mon Sep 17 00:00:00 2001 From: bijianing97 <826015751@qq.com> Date: Wed, 25 Mar 2026 11:34:46 +0800 Subject: [PATCH] Add dlmm add liquidity one side function --- meta.go | 75 +++++++------ metaoradlmm.go | 293 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 326 insertions(+), 42 deletions(-) diff --git a/meta.go b/meta.go index 65bfdf0..e646ee4 100644 --- a/meta.go +++ b/meta.go @@ -68,41 +68,46 @@ var ( ) var ( - meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair2") - meteoraInitializeLbPairEventDiscriminator = calculateDiscriminator("event:LbPairCreate") - meteoraDlmmSwapDiscriminator = calculateDiscriminator("global:swap") - meteoraDlmmSwap2Discriminator = calculateDiscriminator("global:swap2") - meteoraDlmmSwapExactOutDiscriminator = calculateDiscriminator("global:swap_exact_out") - meteoraDlmmSwapExactOut2Discriminator = calculateDiscriminator("global:swap_exact_out2") - meteoraDlmmSwapWithPriceImpactDiscriminator = calculateDiscriminator("global:swap_with_price_impact") - meteoraDlmmSwapWithPriceImpact2Discriminator = calculateDiscriminator("global:swap_with_price_impact2") - meteoraDlmmInitializePositionDiscriminator = calculateDiscriminator("global:initialize_position") - meteoraDlmmInitializePosition2Discriminator = calculateDiscriminator("global:initialize_position2") - meteoraDlmmInitializePositionByOperatorDiscriminator = calculateDiscriminator("global:initialize_position_by_operator") - meteoraDlmmInitializePositionPdaDiscriminator = calculateDiscriminator("global:initialize_position_pda") - meteoraDlmmClosePositionDiscriminator = calculateDiscriminator("global:close_position") - meteoraDlmmClosePosition2Discriminator = calculateDiscriminator("global:close_position2") - meteoraDlmmClosePositionIfEmptyDiscriminator = calculateDiscriminator("global:close_position_if_empty") - meteoraDlmmSwapEventDiscriminator = calculateDiscriminator("event:Swap") - meteoraDlmmAddLiquidityDiscriminator = calculateDiscriminator("global:add_liquidity") - meteoraDlmmAddLiquidity2Discriminator = calculateDiscriminator("global:add_liquidity2") - meteoraDlmmAddLiquidityByStrategyDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy") - meteoraDlmmAddLiquidityByStrategy2Discriminator = calculateDiscriminator("global:add_liquidity_by_strategy2") - meteoraDlmmAddLiquidityByWeightDiscriminator = calculateDiscriminator("global:add_liquidity_by_weight") - meteoraDlmmClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee") - meteoraDlmmClaimFee2Discriminator = calculateDiscriminator("global:claim_fee2") - meteoraDlmmRebalanceLiquidityDiscriminator = calculateDiscriminator("global:rebalance_liquidity") - meteoraDlmmRemoveLiquidityDiscriminator = calculateDiscriminator("global:remove_liquidity") - meteoraDlmmRemoveLiquidity2Discriminator = calculateDiscriminator("global:remove_liquidity2") - meteoraDlmmRemoveLiquidityByRangeDiscriminator = calculateDiscriminator("global:remove_liquidity_by_range") - meteoraDlmmRemoveLiquidityByRange2Discriminator = calculateDiscriminator("global:remove_liquidity_by_range2") - meteoraDlmmAddLiquidityEventDiscriminator = calculateDiscriminator("event:AddLiquidity") - meteoraDlmmClaimFeeEventDiscriminator = calculateDiscriminator("event:ClaimFee") - meteoraDlmmClaimFee2EventDiscriminator = calculateDiscriminator("event:ClaimFee2") - meteoraDlmmPositionCloseEventDiscriminator = calculateDiscriminator("event:PositionClose") - meteoraDlmmPositionCreateEventDiscriminator = calculateDiscriminator("event:PositionCreate") - meteoraDlmmRebalancingEventDiscriminator = calculateDiscriminator("event:Rebalancing") - meteoraDlmmRemoveLiquidityEventDiscriminator = calculateDiscriminator("event:RemoveLiquidity") + meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair2") + meteoraInitializeLbPairEventDiscriminator = calculateDiscriminator("event:LbPairCreate") + meteoraDlmmSwapDiscriminator = calculateDiscriminator("global:swap") + meteoraDlmmSwap2Discriminator = calculateDiscriminator("global:swap2") + meteoraDlmmSwapExactOutDiscriminator = calculateDiscriminator("global:swap_exact_out") + meteoraDlmmSwapExactOut2Discriminator = calculateDiscriminator("global:swap_exact_out2") + meteoraDlmmSwapWithPriceImpactDiscriminator = calculateDiscriminator("global:swap_with_price_impact") + meteoraDlmmSwapWithPriceImpact2Discriminator = calculateDiscriminator("global:swap_with_price_impact2") + meteoraDlmmInitializePositionDiscriminator = calculateDiscriminator("global:initialize_position") + meteoraDlmmInitializePosition2Discriminator = calculateDiscriminator("global:initialize_position2") + meteoraDlmmInitializePositionByOperatorDiscriminator = calculateDiscriminator("global:initialize_position_by_operator") + meteoraDlmmInitializePositionPdaDiscriminator = calculateDiscriminator("global:initialize_position_pda") + meteoraDlmmClosePositionDiscriminator = calculateDiscriminator("global:close_position") + meteoraDlmmClosePosition2Discriminator = calculateDiscriminator("global:close_position2") + meteoraDlmmClosePositionIfEmptyDiscriminator = calculateDiscriminator("global:close_position_if_empty") + meteoraDlmmSwapEventDiscriminator = calculateDiscriminator("event:Swap") + meteoraDlmmAddLiquidityDiscriminator = calculateDiscriminator("global:add_liquidity") + meteoraDlmmAddLiquidity2Discriminator = calculateDiscriminator("global:add_liquidity2") + meteoraDlmmAddLiquidityByStrategyDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy") + meteoraDlmmAddLiquidityByStrategy2Discriminator = calculateDiscriminator("global:add_liquidity_by_strategy2") + meteoraDlmmAddLiquidityByWeightDiscriminator = calculateDiscriminator("global:add_liquidity_by_weight") + meteoraDlmmAddLiquidityOneSideDiscriminator = calculateDiscriminator("global:add_liquidity_one_side") + meteoraDlmmAddLiquidityOneSidePreciseDiscriminator = calculateDiscriminator("global:add_liquidity_one_side_precise") + meteoraDlmmAddLiquidityOneSidePrecise2Discriminator = calculateDiscriminator("global:add_liquidity_one_side_precise2") + meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator = calculateDiscriminator("global:add_liquidity_by_strategy_one_side") + meteoraDlmmClaimFeeDiscriminator = calculateDiscriminator("global:claim_fee") + meteoraDlmmClaimFee2Discriminator = calculateDiscriminator("global:claim_fee2") + meteoraDlmmRebalanceLiquidityDiscriminator = calculateDiscriminator("global:rebalance_liquidity") + meteoraDlmmRemoveAllLiquidityDiscriminator = calculateDiscriminator("global:remove_all_liquidity") + meteoraDlmmRemoveLiquidityDiscriminator = calculateDiscriminator("global:remove_liquidity") + meteoraDlmmRemoveLiquidity2Discriminator = calculateDiscriminator("global:remove_liquidity2") + meteoraDlmmRemoveLiquidityByRangeDiscriminator = calculateDiscriminator("global:remove_liquidity_by_range") + meteoraDlmmRemoveLiquidityByRange2Discriminator = calculateDiscriminator("global:remove_liquidity_by_range2") + meteoraDlmmAddLiquidityEventDiscriminator = calculateDiscriminator("event:AddLiquidity") + meteoraDlmmClaimFeeEventDiscriminator = calculateDiscriminator("event:ClaimFee") + meteoraDlmmClaimFee2EventDiscriminator = calculateDiscriminator("event:ClaimFee2") + meteoraDlmmPositionCloseEventDiscriminator = calculateDiscriminator("event:PositionClose") + meteoraDlmmPositionCreateEventDiscriminator = calculateDiscriminator("event:PositionCreate") + meteoraDlmmRebalancingEventDiscriminator = calculateDiscriminator("event:Rebalancing") + meteoraDlmmRemoveLiquidityEventDiscriminator = calculateDiscriminator("event:RemoveLiquidity") ) var ( diff --git a/metaoradlmm.go b/metaoradlmm.go index 4a87cf2..8062068 100644 --- a/metaoradlmm.go +++ b/metaoradlmm.go @@ -178,6 +178,53 @@ type dlmmAddLiquidityByWeightArgs struct { LiquidityParameter dlmmLiquidityParameterByWeight } +type dlmmLiquidityOneSideParameter struct { + Amount uint64 + ActiveID int32 + MaxActiveBinSlippage int32 + BinLiquidityDist []dlmmBinLiquidityDistributionByWeight +} + +type dlmmLiquidityParameterByStrategyOneSide struct { + Amount uint64 + ActiveID int32 + MaxActiveBinSlippage int32 + StrategyParameters dlmmStrategyParameters +} + +type dlmmAddLiquidityOneSideArgs struct { + LiquidityParameter dlmmLiquidityOneSideParameter +} + +type dlmmAddLiquidityByStrategyOneSideArgs struct { + LiquidityParameter dlmmLiquidityParameterByStrategyOneSide +} + +type dlmmCompressedBinDepositAmount struct { + BinID int32 + Amount uint32 +} + +type dlmmAddLiquiditySingleSidePreciseParameter struct { + Bins []dlmmCompressedBinDepositAmount + DecompressMultiplier uint64 +} + +type dlmmAddLiquiditySingleSidePreciseParameter2 struct { + Bins []dlmmCompressedBinDepositAmount + DecompressMultiplier uint64 + MaxAmount uint64 +} + +type dlmmAddLiquidityOneSidePreciseArgs struct { + Parameter dlmmAddLiquiditySingleSidePreciseParameter +} + +type dlmmAddLiquidityOneSidePrecise2Args struct { + LiquidityParameter dlmmAddLiquiditySingleSidePreciseParameter2 + RemainingAccountsInfo dlmmRemainingAccountsInfo +} + type dlmmRemoveLiquidityArgs struct { BinLiquidityRemoval []dlmmBinLiquidityReduction } @@ -264,6 +311,16 @@ type dlmmLiquidityAccounts struct { tokenYProgramIdx int } +type dlmmOneSideLiquidityAccounts struct { + positionIdx int + poolIdx int + userTokenIdx int + reserveIdx int + tokenMintIdx int + userIdx int + tokenProgramIdx int +} + var meteoraDlmmEventAuthority = func() solana.PublicKey { key, _, err := solana.FindProgramAddress([][]byte{[]byte("__event_authority")}, meteoraDlmmProgram) if err != nil { @@ -294,13 +351,15 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI return metaoradlmmSwap2Parser(tx, instruction, innerInstructions, offset) case meteoraDlmmAddLiquidityDiscriminator, meteoraDlmmAddLiquidity2Discriminator, meteoraDlmmAddLiquidityByStrategyDiscriminator, meteoraDlmmAddLiquidityByStrategy2Discriminator, - meteoraDlmmAddLiquidityByWeightDiscriminator: + meteoraDlmmAddLiquidityByWeightDiscriminator, meteoraDlmmAddLiquidityOneSideDiscriminator, + meteoraDlmmAddLiquidityOneSidePreciseDiscriminator, meteoraDlmmAddLiquidityOneSidePrecise2Discriminator, + meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator: return metaoradlmmAddLiquidityParser(tx, instruction, innerInstructions, offset) case meteoraDlmmClaimFeeDiscriminator, meteoraDlmmClaimFee2Discriminator: return metaoradlmmClaimFeeParser(tx, instruction, innerInstructions, offset) case meteoraDlmmRebalanceLiquidityDiscriminator: return metaoradlmmRebalanceLiquidityParser(tx, instruction, innerInstructions, offset) - case meteoraDlmmRemoveLiquidityDiscriminator, meteoraDlmmRemoveLiquidity2Discriminator, + case meteoraDlmmRemoveAllLiquidityDiscriminator, meteoraDlmmRemoveLiquidityDiscriminator, meteoraDlmmRemoveLiquidity2Discriminator, meteoraDlmmRemoveLiquidityByRangeDiscriminator, meteoraDlmmRemoveLiquidityByRange2Discriminator: return metaoradlmmRemoveLiquidityParser(tx, instruction, innerInstructions, offset) case meteoraDlmmClosePositionDiscriminator, meteoraDlmmClosePosition2Discriminator, meteoraDlmmClosePositionIfEmptyDiscriminator: @@ -730,6 +789,7 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc startBinId int32 endBinId int32 hasRange bool + oneSide bool ) switch discriminator { @@ -783,15 +843,44 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc weightDist = args.LiquidityParameter.BinLiquidityDist startBinId, endBinId = dlmmMinMaxBinIdFromWeightDistribution(weightDist) hasRange = len(weightDist) > 0 + case meteoraDlmmAddLiquidityOneSideDiscriminator: + var args dlmmAddLiquidityOneSideArgs + if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity one side decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + weightDist = args.LiquidityParameter.BinLiquidityDist + startBinId, endBinId = dlmmMinMaxBinIdFromWeightDistribution(weightDist) + hasRange = len(weightDist) > 0 + oneSide = true + case meteoraDlmmAddLiquidityOneSidePreciseDiscriminator: + var args dlmmAddLiquidityOneSidePreciseArgs + if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity one side precise decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + startBinId, endBinId = dlmmMinMaxBinIDFromCompressedDeposits(args.Parameter.Bins) + hasRange = len(args.Parameter.Bins) > 0 + oneSide = true + case meteoraDlmmAddLiquidityOneSidePrecise2Discriminator: + var args dlmmAddLiquidityOneSidePrecise2Args + if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity one side precise2 decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + startBinId, endBinId = dlmmMinMaxBinIDFromCompressedDeposits(args.LiquidityParameter.Bins) + hasRange = len(args.LiquidityParameter.Bins) > 0 + oneSide = true + case meteoraDlmmAddLiquidityByStrategyOneSideDiscriminator: + var args dlmmAddLiquidityByStrategyOneSideArgs + if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity by strategy one side decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + startBinId = args.LiquidityParameter.StrategyParameters.MinBinId + endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId + hasRange = true + oneSide = true default: return nil, increaseOffset(offset), InstructionIgnoredError } - accounts, err := resolveDlmmLiquidityAccounts(result, instruction.Accounts) - if err != nil { - return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1]) - } - addEvent, nextOffset, err := dlmmAddLiquidityEventFromInnerInstructions(innerInstructions, instruction, offset) if err != nil { return nil, nextOffset, err @@ -800,6 +889,19 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc amountX = addEvent.Amounts[0] amountY = addEvent.Amounts[1] + if oneSide { + swaps, err := dlmmBuildOneSideAddSwap(tx, instruction, addEvent, weightDist, startBinId, endBinId, hasRange, entryContract) + if err != nil { + return nil, offset, err + } + return swaps, offset, nil + } + + accounts, err := resolveDlmmLiquidityAccounts(result, instruction.Accounts) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm add liquidity accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + binChanges := []DlmmBinLiquidityChange(nil) if len(binDist) > 0 { binChanges = dlmmBinChangesFromDistribution(amountX, amountY, binDist) @@ -936,6 +1038,8 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst ) switch discriminator { + case meteoraDlmmRemoveAllLiquidityDiscriminator: + removeBp = 10000 case meteoraDlmmRemoveLiquidityDiscriminator: var args dlmmRemoveLiquidityArgs if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { @@ -1822,6 +1926,164 @@ func resolveDlmmLiquidityAccounts(result *RawTx, accounts []int) (dlmmLiquidityA }, nil } +func resolveDlmmOneSideLiquidityAccounts(result *RawTx, accounts []int) (dlmmOneSideLiquidityAccounts, error) { + if len(accounts) < 10 { + return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("accounts too short, expected at least 10") + } + accountList := result.accountList + + eventAuthorityPos := -1 + for i, idx := range accounts { + if idx < 0 || idx >= len(accountList) { + continue + } + if accountList[idx].Equals(meteoraDlmmEventAuthority) { + eventAuthorityPos = i + break + } + } + if eventAuthorityPos == -1 { + return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("event authority not found") + } + if eventAuthorityPos+1 >= len(accounts) || !accountList[accounts[eventAuthorityPos+1]].Equals(meteoraDlmmProgram) { + return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("program id not found after event authority") + } + + tokenProgramPos := eventAuthorityPos - 1 + userPos := eventAuthorityPos - 2 + if tokenProgramPos < 0 || userPos < 0 { + return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("one side liquidity account positions invalid") + } + if len(accounts) < 6 { + return dlmmOneSideLiquidityAccounts{}, fmt.Errorf("accounts too short for one side liquidity parsing") + } + + return dlmmOneSideLiquidityAccounts{ + positionIdx: accounts[0], + poolIdx: accounts[1], + userTokenIdx: accounts[3], + reserveIdx: accounts[4], + tokenMintIdx: accounts[5], + userIdx: accounts[userPos], + tokenProgramIdx: accounts[tokenProgramPos], + }, nil +} + +func dlmmBuildOneSideAddSwap( + tx *Tx, + instruction Instruction, + addEvent dlmmAddLiquidityEvent, + weightDist []dlmmBinLiquidityDistributionByWeight, + startBinId int32, + endBinId int32, + hasRange bool, + entryContract solana.PublicKey, +) ([]Swap, error) { + result := tx.rawTx + accounts, err := resolveDlmmOneSideLiquidityAccounts(result, instruction.Accounts) + if err != nil { + return nil, err + } + + knownMint := result.accountList[accounts.tokenMintIdx] + knownTokenProgram := result.accountList[accounts.tokenProgramIdx] + knownDecimals, ok := dlmmTokenDecimals(result, accounts.reserveIdx) + if !ok { + knownDecimals, _ = dlmmTokenDecimals(result, accounts.userTokenIdx) + } + knownReserveBalance := getAccountBalanceAfterTx(result, accounts.reserveIdx) + knownUserBalance := getAccountBalanceAfterTx(result, accounts.userTokenIdx) + if knownMint.Equals(wSolMint) { + if solAmount, err := GetSolAfterTx(result, accounts.userIdx); err == nil { + knownUserBalance = knownUserBalance.Add(decimal.NewFromUint64(solAmount)) + } + } + + binChanges := []DlmmBinLiquidityChange(nil) + if len(weightDist) > 0 { + binChanges = dlmmBinChangesFromRange(startBinId, endBinId, 0) + } else if hasRange { + binChanges = dlmmBinChangesFromRange(startBinId, endBinId, 0) + } + + eventUser := result.accountList[accounts.userIdx] + if !addEvent.From.IsZero() { + eventUser = addEvent.From + } + positionAccount := result.accountList[accounts.positionIdx] + if !addEvent.Position.IsZero() { + positionAccount = addEvent.Position + } + + swap := Swap{ + Program: SolProgramMeteoraDLMM, + Event: "add", + Pool: result.accountList[accounts.poolIdx], + User: eventUser, + EntryContract: entryContract, + ActiveBinId: addEvent.ActiveBinId, + StartBinId: startBinId, + EndBinId: endBinId, + BinChanges: binChanges, + PositionAccount: positionAccount, + } + + knownIsX := dlmmInferOneSideLiquidityAxis(result, accounts, addEvent) + if knownIsX { + swap.BaseMint = knownMint + swap.BaseTokenProgram = knownTokenProgram + swap.BaseMintDecimals = knownDecimals + swap.BaseAmount = decimal.NewFromUint64(addEvent.Amounts[0]) + swap.BaseReserve = knownReserveBalance + swap.UserBaseBalance = knownUserBalance + if _, exists := tx.Token[knownMint]; !exists && !knownMint.Equals(wSolMint) { + tx.Token[knownMint] = TokenMeta{ + Mint: knownMint, + Decimals: knownDecimals, + TokenProgram: knownTokenProgram, + } + } + } else { + swap.QuoteMint = knownMint + swap.QuoteTokenProgram = knownTokenProgram + swap.QuoteMintDecimals = knownDecimals + swap.QuoteAmount = decimal.NewFromUint64(addEvent.Amounts[1]) + swap.QuoteReserve = knownReserveBalance + swap.UserQuoteBalance = knownUserBalance + if _, exists := tx.Token[knownMint]; !exists && !knownMint.Equals(wSolMint) { + tx.Token[knownMint] = TokenMeta{ + Mint: knownMint, + Decimals: knownDecimals, + TokenProgram: knownTokenProgram, + } + } + } + + return []Swap{swap}, nil +} + +func dlmmInferOneSideLiquidityAxis(result *RawTx, accounts dlmmOneSideLiquidityAccounts, addEvent dlmmAddLiquidityEvent) bool { + knownAmount, ok := dlmmTokenDelta(result, accounts.reserveIdx) + if !ok || knownAmount.IsZero() { + knownAmount, _ = dlmmTokenDelta(result, accounts.userTokenIdx) + } + + amountX := decimal.NewFromUint64(addEvent.Amounts[0]) + amountY := decimal.NewFromUint64(addEvent.Amounts[1]) + switch { + case !knownAmount.IsZero() && knownAmount.Equal(amountX) && !knownAmount.Equal(amountY): + return true + case !knownAmount.IsZero() && knownAmount.Equal(amountY) && !knownAmount.Equal(amountX): + return false + case addEvent.Amounts[0] > 0 && addEvent.Amounts[1] == 0: + return true + case addEvent.Amounts[1] > 0 && addEvent.Amounts[0] == 0: + return false + default: + return true + } +} + func resolveDlmmClaimFeeAccounts(result *RawTx, data []byte, accounts []int) (dlmmLiquidityAccounts, error) { if len(data) < 8 { return dlmmLiquidityAccounts{}, fmt.Errorf("instruction data too short") @@ -2045,6 +2307,23 @@ func dlmmBinChangesFromRange(startBinId, endBinId int32, bpsToRemove uint16) []D return changes } +func dlmmMinMaxBinIDFromCompressedDeposits(bins []dlmmCompressedBinDepositAmount) (startBinID, endBinID int32) { + if len(bins) == 0 { + return 0, 0 + } + startBinID = bins[0].BinID + endBinID = bins[0].BinID + for _, bin := range bins[1:] { + if bin.BinID < startBinID { + startBinID = bin.BinID + } + if bin.BinID > endBinID { + endBinID = bin.BinID + } + } + return startBinID, endBinID +} + func dlmmCommonRemoveBp(reduction []dlmmBinLiquidityReduction) int32 { if len(reduction) == 0 { return 0