diff --git a/cmd/rpc_parse/main.go b/cmd/rpc_parse/main.go new file mode 100644 index 0000000..842cab2 --- /dev/null +++ b/cmd/rpc_parse/main.go @@ -0,0 +1,121 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + pump_parser "github.com/thloyi/pump-parser" +) + +func main() { + const rpcURL = "https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d" + txHash := os.Getenv("TX_HASH") + if txHash == "" { + txHash = "2AhpL5KhVmG3D38CwMzrHuRyTucEQ43GzBXL2mo5WiugdZMVmK1dtX8brGe3sxvvFDY6iSSviJTvqCtr4UL3Pc7J" + } + + if txHash == "" { + fmt.Fprintln(os.Stderr, "txHash is empty; set it in cmd/rpc_parse/main.go") + os.Exit(1) + } + + sig, err := solana.SignatureFromBase58(txHash) + if err != nil { + fmt.Fprintf(os.Stderr, "invalid txHash: %v\n", err) + os.Exit(1) + } + + client := rpc.New(rpcURL) + maxSupportedVersion := uint64(0) + out, err := client.GetTransaction(context.Background(), sig, &rpc.GetTransactionOpts{ + Encoding: solana.EncodingBase64, + Commitment: rpc.CommitmentConfirmed, + MaxSupportedTransactionVersion: &maxSupportedVersion, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "rpc getTransaction error: %v\n", err) + os.Exit(1) + } + if out == nil || out.Transaction == nil || out.Meta == nil { + fmt.Fprintln(os.Stderr, "rpc getTransaction returned empty response") + os.Exit(1) + } + + rawBinary := out.Transaction.GetBinary() + if len(rawBinary) == 0 { + fmt.Fprintln(os.Stderr, "rpc getTransaction returned empty transaction data") + os.Exit(1) + } + + txWithMeta := rpc.TransactionWithMeta{ + Slot: out.Slot, + BlockTime: out.BlockTime, + Transaction: rpc.DataBytesOrJSONFromBytes(rawBinary), + Meta: out.Meta, + Version: out.Version, + } + + var blockTime *uint64 + if out.BlockTime != nil { + bt := uint64(*out.BlockTime) + blockTime = &bt + } + + rawTx, err := pump_parser.FromRpcTransactionWithMeta(txWithMeta, blockTime, out.Slot, 0) + if err != nil { + fmt.Fprintf(os.Stderr, "convert rpc transaction error: %v\n", err) + os.Exit(1) + } + + pump_parser.EnableAllParsers() + + parsed, err := pump_parser.ParseRawTx(rawTx) + if err != nil { + fmt.Fprintf(os.Stderr, "parse raw tx error: %v\n", err) + os.Exit(1) + } + if len(parsed.Swaps) == 0 { + fmt.Println("no swaps parsed from tx") + return + } + + for i, swap := range parsed.Swaps { + fmt.Printf("swap[%d]\n", i) + fmt.Printf(" program: %s\n", swap.Program) + fmt.Printf(" event: %s\n", swap.Event) + fmt.Printf(" pool: %s\n", swap.Pool) + fmt.Printf(" user: %s\n", swap.User) + fmt.Printf(" base_mint: %s (decimals=%d)\n", swap.BaseMint, swap.BaseMintDecimals) + fmt.Printf(" quote_mint: %s (decimals=%d)\n", swap.QuoteMint, swap.QuoteMintDecimals) + fmt.Printf(" base_amount: %s\n", swap.BaseAmount.String()) + fmt.Printf(" quote_amount: %s\n", swap.QuoteAmount.String()) + if !swap.FeeAmount.IsZero() || swap.FeeSide != "" { + fmt.Printf(" fee_amount: %s\n", swap.FeeAmount.String()) + fmt.Printf(" lp_fee_amount: %s\n", swap.LpFeeAmount.String()) + fmt.Printf(" fee_side: %s\n", swap.FeeSide) + fmt.Printf(" fee_mint: %s (decimals=%d)\n", swap.FeeMint, swap.FeeMintDecimals) + fmt.Printf(" fee_token_program: %s\n", swap.FeeTokenProgram) + } + fmt.Printf(" base_reserve: %s\n", swap.BaseReserve.String()) + fmt.Printf(" quote_reserve: %s\n", swap.QuoteReserve.String()) + fmt.Printf(" base_token_program: %s\n", swap.BaseTokenProgram) + fmt.Printf(" quote_token_program: %s\n", swap.QuoteTokenProgram) + fmt.Printf(" entry_contract: %s\n", swap.EntryContract) + fmt.Printf(" user_base_balance: %s\n", swap.UserBaseBalance.String()) + fmt.Printf(" user_quote_balance: %s\n", swap.UserQuoteBalance.String()) + fmt.Printf(" active_bin_id: %d\n", swap.ActiveBinId) + fmt.Printf(" start_bin_id: %d\n", swap.StartBinId) + fmt.Printf(" end_bin_id: %d\n", swap.EndBinId) + fmt.Printf(" remove_bp: %d\n", swap.RemoveBp) + fmt.Printf(" position_account: %s\n", swap.PositionAccount) + if swap.Mayhem { + fmt.Printf(" mayhem: true\n") + } else { + fmt.Printf(" mayhem: false\n") + } + fmt.Println() + } +} diff --git a/meta.go b/meta.go index e646ee4..852b164 100644 --- a/meta.go +++ b/meta.go @@ -68,46 +68,50 @@ 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") - 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") + meteoraInitializeCustomizablePermissionlessLbPairDiscriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair") + meteoraInitializeCustomizablePermissionlessLbPair2Discriminator = calculateDiscriminator("global:initialize_customizable_permissionless_lb_pair2") + meteoraInitializeLbPairDiscriminator = calculateDiscriminator("global:initialize_lb_pair") + meteoraInitializeLbPair2Discriminator = calculateDiscriminator("global:initialize_lb_pair2") + meteoraInitializePermissionLbPairDiscriminator = calculateDiscriminator("global:initialize_permission_lb_pair") + meteoraInitializeLbPairEventDiscriminator = calculateDiscriminator("event:LbPairCreate") + meteoraDlmmSwapDiscriminator = calculateDiscriminator("global:swap") + meteoraDlmmSwap2Discriminator = calculateDiscriminator("global:swap2") + 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 8062068..e0d8bd8 100644 --- a/metaoradlmm.go +++ b/metaoradlmm.go @@ -66,6 +66,13 @@ type dlmmPositionCloseEvent struct { Owner solana.PublicKey } +type dlmmLbPairCreateEvent struct { + LbPair solana.PublicKey + BinStep uint16 + TokenX solana.PublicKey + TokenY solana.PublicKey +} + type dlmmClaimFeeInnerEvent struct { LbPair solana.PublicKey Position solana.PublicKey @@ -340,7 +347,11 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI discriminator := *(*[8]byte)(decode[:8]) switch discriminator { - case meteoraInitializeLbPairDiscriminator: + case meteoraInitializeCustomizablePermissionlessLbPairDiscriminator, + meteoraInitializeCustomizablePermissionlessLbPair2Discriminator, + meteoraInitializeLbPairDiscriminator, + meteoraInitializeLbPair2Discriminator, + meteoraInitializePermissionLbPairDiscriminator: return metaoradlmmInitializeParser(tx, instruction, innerInstructions, offset) case meteoraDlmmInitializePositionDiscriminator, meteoraDlmmInitializePosition2Discriminator, meteoraDlmmInitializePositionByOperatorDiscriminator, meteoraDlmmInitializePositionPdaDiscriminator: @@ -369,53 +380,131 @@ func metaoradlmmParser(tx *Tx, instruction Instruction, innerInstructions InnerI } } +type dlmmInitializeAccounts struct { + pool solana.PublicKey + token0 solana.PublicKey + token1 solana.PublicKey + baseTokenProgram solana.PublicKey + quoteTokenProgram solana.PublicKey + user solana.PublicKey +} + +func resolveDlmmInitializeAccounts(result *RawTx, data []byte, accounts []int) (dlmmInitializeAccounts, error) { + if len(data) < 8 { + return dlmmInitializeAccounts{}, fmt.Errorf("instruction data too short") + } + + accountList := result.getAccountList() + resolveAt := func(position int) (solana.PublicKey, error) { + if position < 0 || position >= len(accounts) { + return solana.PublicKey{}, fmt.Errorf("accounts too short, missing position %d", position) + } + accountIndex := accounts[position] + if accountIndex < 0 || accountIndex >= len(accountList) { + return solana.PublicKey{}, fmt.Errorf("account index out of range at position %d", position) + } + return accountList[accountIndex], nil + } + + resolveCommon := func(poolPos, token0Pos, token1Pos, userPos, baseTokenProgramPos, quoteTokenProgramPos int) (dlmmInitializeAccounts, error) { + pool, err := resolveAt(poolPos) + if err != nil { + return dlmmInitializeAccounts{}, err + } + token0, err := resolveAt(token0Pos) + if err != nil { + return dlmmInitializeAccounts{}, err + } + token1, err := resolveAt(token1Pos) + if err != nil { + return dlmmInitializeAccounts{}, err + } + baseTokenProgram, err := resolveAt(baseTokenProgramPos) + if err != nil { + return dlmmInitializeAccounts{}, err + } + quoteTokenProgram, err := resolveAt(quoteTokenProgramPos) + if err != nil { + return dlmmInitializeAccounts{}, err + } + user, err := resolveAt(userPos) + if err != nil { + return dlmmInitializeAccounts{}, err + } + + return dlmmInitializeAccounts{ + pool: pool, + token0: token0, + token1: token1, + baseTokenProgram: baseTokenProgram, + quoteTokenProgram: quoteTokenProgram, + user: user, + }, nil + } + + discriminator := *(*[8]byte)(data[:8]) + switch discriminator { + case meteoraInitializeLbPairDiscriminator, + meteoraInitializeCustomizablePermissionlessLbPairDiscriminator: + return resolveCommon(0, 2, 3, 8, 9, 9) + case meteoraInitializeLbPair2Discriminator, + meteoraInitializeCustomizablePermissionlessLbPair2Discriminator: + return resolveCommon(0, 2, 3, 8, 11, 12) + case meteoraInitializePermissionLbPairDiscriminator: + return resolveCommon(1, 3, 4, 8, 11, 12) + default: + return dlmmInitializeAccounts{}, fmt.Errorf("unsupported initialize discriminator") + } +} + func metaoradlmmInitializeParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { - market := tx.rawTx.accountList[instruction.Accounts[0]] - token0 := tx.rawTx.accountList[instruction.Accounts[2]] - token1 := tx.rawTx.accountList[instruction.Accounts[3]] + accounts, err := resolveDlmmInitializeAccounts(tx.rawTx, instruction.Data, instruction.Accounts) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm initialize accounts parse error: %v, offset, %d, %d", err, offset[0], offset[1]) + } entryContract := tx.rawTx.accountList[tx.rawTx.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] - var baseDecimals uint8 - var quoteDecimals uint8 - for _, acc := range tx.rawTx.Meta.PostTokenBalances { - if acc.MintAccount.Equals(token0) { - baseDecimals = uint8(acc.UITokenAmount.Decimals) - } - if acc.MintAccount.Equals(token1) { - quoteDecimals = uint8(acc.UITokenAmount.Decimals) + findMintDecimals := func(mint solana.PublicKey) uint8 { + for _, acc := range tx.rawTx.Meta.PostTokenBalances { + if acc.MintAccount.Equals(mint) { + return uint8(acc.UITokenAmount.Decimals) + } } + return 0 } + swap := Swap{ Program: SolProgramMeteoraDLMM, Event: "create", - Pool: market, - BaseMint: token0, - QuoteMint: token1, - BaseTokenProgram: tx.rawTx.accountList[instruction.Accounts[11]], - QuoteTokenProgram: tx.rawTx.accountList[instruction.Accounts[12]], + Pool: accounts.pool, + BaseMint: accounts.token0, + QuoteMint: accounts.token1, + BaseTokenProgram: accounts.baseTokenProgram, + QuoteTokenProgram: accounts.quoteTokenProgram, Creator: tx.rawTx.accountList[0], - BaseMintDecimals: baseDecimals, - QuoteMintDecimals: quoteDecimals, - User: tx.rawTx.accountList[instruction.Accounts[8]], + BaseMintDecimals: findMintDecimals(accounts.token0), + QuoteMintDecimals: findMintDecimals(accounts.token1), + User: accounts.user, EntryContract: entryContract, } - var prefixLen = offset[1] - inners, err := getInnerInstructions(innerInstructions, prefixLen) + createEvent, nextOffset, found, err := dlmmLbPairCreateEventFromInnerInstructions(innerInstructions, instruction, offset) if err != nil { - return nil, increaseOffset(offset), fmt.Errorf("pump create get inner instructions error: %v, offset, %d, %d", err, offset[0], offset[1]) + return nil, nextOffset, err } - var programIndex = instruction.ProgramIDIndex - - for innerIndex, innerInstr := range inners { - if innerInstr.ProgramIDIndex == programIndex && len(innerInstr.Data) >= 16 && bytes.Equal(innerInstr.Data[:8], pumpEventDiscriminator[:]) && bytes.Equal(innerInstr.Data[8:16], meteoraInitializeLbPairEventDiscriminator[:]) { - if offset[1] == 0 { - offset[0] += 1 - } else { - offset[1] = uint(innerIndex) + 1 + prefixLen - } - break + if found { + offset = nextOffset + if !createEvent.LbPair.IsZero() { + swap.Pool = createEvent.LbPair } + if !createEvent.TokenX.IsZero() { + swap.BaseMint = createEvent.TokenX + } + if !createEvent.TokenY.IsZero() { + swap.QuoteMint = createEvent.TokenY + } + swap.BaseMintDecimals = findMintDecimals(swap.BaseMint) + swap.QuoteMintDecimals = findMintDecimals(swap.QuoteMint) } return []Swap{swap}, offset, nil } @@ -729,6 +818,18 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In userQuote = userQuote.Add(decimal.NewFromUint64(solAmount)) } } + feeAmount, feeSide, feeMint, feeTokenProgram, feeDecimals := dlmmSwapFeeInfo( + baseIsX, + swapForY, + swapEvent.Fee, + baseMint, + quoteMint, + baseTokenProgram, + quoteTokenProgram, + baseDecimals, + quoteDecimals, + ) + lpFeeAmount := dlmmSwapLpFeeAmount(swapEvent.Fee, swapEvent.ProtocolFee, swapEvent.HostFee) swap := Swap{ Program: SolProgramMeteoraDLMM, @@ -744,6 +845,12 @@ func metaoradlmmSwapParser(tx *Tx, instruction Instruction, innerInstructions In User: eventUser, BaseAmount: baseAmount, QuoteAmount: quoteAmount, + FeeAmount: feeAmount, + LpFeeAmount: lpFeeAmount, + FeeSide: feeSide, + FeeMint: feeMint, + FeeTokenProgram: feeTokenProgram, + FeeMintDecimals: feeDecimals, BaseReserve: baseReserve, QuoteReserve: quoteReserve, UserBaseBalance: userBase, @@ -761,6 +868,35 @@ func metaoradlmmSwap2Parser(tx *Tx, instruction Instruction, innerInstructions I return metaoradlmmSwapParser(tx, instruction, innerInstructions, offset) } +func dlmmSwapFeeInfo( + baseIsX bool, + swapForY bool, + fee uint64, + baseMint solana.PublicKey, + quoteMint solana.PublicKey, + baseTokenProgram solana.PublicKey, + quoteTokenProgram solana.PublicKey, + baseDecimals uint8, + quoteDecimals uint8, +) (decimal.Decimal, string, solana.PublicKey, solana.PublicKey, uint8) { + feeAmount := decimal.NewFromUint64(fee) + if baseIsX == swapForY { + return feeAmount, "base", baseMint, baseTokenProgram, baseDecimals + } + return feeAmount, "quote", quoteMint, quoteTokenProgram, quoteDecimals +} + +func dlmmSwapLpFeeAmount(fee, protocolFee, hostFee uint64) decimal.Decimal { + total := decimal.NewFromUint64(fee) + protocol := decimal.NewFromUint64(protocolFee) + host := decimal.NewFromUint64(hostFee) + lpFee := total.Sub(protocol).Sub(host) + if lpFee.IsNegative() { + return decimal.Zero + } + return lpFee +} + func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { result := tx.rawTx @@ -788,7 +924,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc weightDist []dlmmBinLiquidityDistributionByWeight startBinId int32 endBinId int32 - hasRange bool oneSide bool ) @@ -802,7 +937,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc amountY = args.LiquidityParameter.AmountY binDist = args.LiquidityParameter.BinLiquidityDist startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist) - hasRange = len(binDist) > 0 case meteoraDlmmAddLiquidity2Discriminator: var args dlmmAddLiquidity2Args if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { @@ -812,7 +946,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc amountY = args.LiquidityParameter.AmountY binDist = args.LiquidityParameter.BinLiquidityDist startBinId, endBinId = dlmmMinMaxBinIdFromDistribution(binDist) - hasRange = len(binDist) > 0 case meteoraDlmmAddLiquidityByStrategyDiscriminator: var args dlmmAddLiquidityByStrategyArgs if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { @@ -822,7 +955,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc amountY = args.LiquidityParameter.AmountY startBinId = args.LiquidityParameter.StrategyParameters.MinBinId endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId - hasRange = true case meteoraDlmmAddLiquidityByStrategy2Discriminator: var args dlmmAddLiquidityByStrategy2Args if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { @@ -832,7 +964,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc amountY = args.LiquidityParameter.AmountY startBinId = args.LiquidityParameter.StrategyParameters.MinBinId endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId - hasRange = true case meteoraDlmmAddLiquidityByWeightDiscriminator: var args dlmmAddLiquidityByWeightArgs if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { @@ -842,7 +973,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc amountY = args.LiquidityParameter.AmountY weightDist = args.LiquidityParameter.BinLiquidityDist startBinId, endBinId = dlmmMinMaxBinIdFromWeightDistribution(weightDist) - hasRange = len(weightDist) > 0 case meteoraDlmmAddLiquidityOneSideDiscriminator: var args dlmmAddLiquidityOneSideArgs if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { @@ -850,7 +980,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc } weightDist = args.LiquidityParameter.BinLiquidityDist startBinId, endBinId = dlmmMinMaxBinIdFromWeightDistribution(weightDist) - hasRange = len(weightDist) > 0 oneSide = true case meteoraDlmmAddLiquidityOneSidePreciseDiscriminator: var args dlmmAddLiquidityOneSidePreciseArgs @@ -858,7 +987,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc 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 @@ -866,7 +994,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc 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 @@ -875,7 +1002,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc } startBinId = args.LiquidityParameter.StrategyParameters.MinBinId endBinId = args.LiquidityParameter.StrategyParameters.MaxBinId - hasRange = true oneSide = true default: return nil, increaseOffset(offset), InstructionIgnoredError @@ -890,7 +1016,7 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc amountY = addEvent.Amounts[1] if oneSide { - swaps, err := dlmmBuildOneSideAddSwap(tx, instruction, addEvent, weightDist, startBinId, endBinId, hasRange, entryContract) + swaps, err := dlmmBuildOneSideAddSwap(tx, instruction, addEvent, startBinId, endBinId, entryContract) if err != nil { return nil, offset, err } @@ -902,16 +1028,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc 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) - } else if len(weightDist) > 0 { - // Weight-only params do not preserve per-side amounts for each bin, so keep the affected range only. - binChanges = dlmmBinChangesFromRange(startBinId, endBinId, 0) - } else if hasRange { - binChanges = dlmmBinChangesFromRange(startBinId, endBinId, 0) - } - pool := result.accountList[accounts.poolIdx] tokenXMint := result.accountList[accounts.tokenXMintIdx] tokenYMint := result.accountList[accounts.tokenYMintIdx] @@ -942,7 +1058,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc baseAmount = amountYDec quoteAmount = amountXDec } - eventUser := result.accountList[accounts.userIdx] if !addEvent.From.IsZero() { eventUser = addEvent.From @@ -1003,7 +1118,6 @@ func metaoradlmmAddLiquidityParser(tx *Tx, instruction Instruction, innerInstruc ActiveBinId: addEvent.ActiveBinId, StartBinId: startBinId, EndBinId: endBinId, - BinChanges: binChanges, PositionAccount: result.accountList[accounts.positionIdx], } @@ -1031,7 +1145,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst discriminator := *(*[8]byte)(decode[:8]) var ( - binChanges []DlmmBinLiquidityChange startBinId int32 endBinId int32 removeBp int32 @@ -1045,7 +1158,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm remove liquidity decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } - binChanges = dlmmBinChangesFromReduction(args.BinLiquidityRemoval) startBinId, endBinId = dlmmMinMaxBinIdFromReduction(args.BinLiquidityRemoval) removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval) case meteoraDlmmRemoveLiquidity2Discriminator: @@ -1053,7 +1165,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { return nil, increaseOffset(offset), fmt.Errorf("meteora dlmm remove liquidity2 decode error: %v, offset, %d, %d", err, offset[0], offset[1]) } - binChanges = dlmmBinChangesFromReduction(args.BinLiquidityRemoval) startBinId, endBinId = dlmmMinMaxBinIdFromReduction(args.BinLiquidityRemoval) removeBp = dlmmCommonRemoveBp(args.BinLiquidityRemoval) case meteoraDlmmRemoveLiquidityByRangeDiscriminator: @@ -1064,7 +1175,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst startBinId = args.FromBinId endBinId = args.ToBinId removeBp = int32(args.BpsToRemove) - binChanges = dlmmBinChangesFromRange(startBinId, endBinId, args.BpsToRemove) case meteoraDlmmRemoveLiquidityByRange2Discriminator: var args dlmmRemoveLiquidityByRange2Args if err := agbinary.NewBorshDecoder(decode[8:]).Decode(&args); err != nil { @@ -1073,7 +1183,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst startBinId = args.FromBinId endBinId = args.ToBinId removeBp = int32(args.BpsToRemove) - binChanges = dlmmBinChangesFromRange(startBinId, endBinId, args.BpsToRemove) default: return nil, increaseOffset(offset), InstructionIgnoredError } @@ -1119,7 +1228,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst baseAmount = amountYDec quoteAmount = amountXDec } - eventUser := result.accountList[accounts.userIdx] if !removeEvent.From.IsZero() { eventUser = removeEvent.From @@ -1181,7 +1289,6 @@ func metaoradlmmRemoveLiquidityParser(tx *Tx, instruction Instruction, innerInst StartBinId: startBinId, EndBinId: endBinId, RemoveBp: removeBp, - BinChanges: binChanges, PositionAccount: result.accountList[accounts.positionIdx], } @@ -1425,7 +1532,6 @@ func metaoradlmmRebalanceLiquidityParser(tx *Tx, instruction Instruction, innerI ActiveBinId: event.ActiveBinId, StartBinId: event.OldMinBinId, EndBinId: event.OldMaxBinId, - BinChanges: dlmmBinChangesFromRange(event.OldMinBinId, event.OldMaxBinId, 0), PositionAccount: result.accountList[accounts.positionIdx], }) } @@ -1451,7 +1557,6 @@ func metaoradlmmRebalanceLiquidityParser(tx *Tx, instruction Instruction, innerI ActiveBinId: event.ActiveBinId, StartBinId: event.NewMinBinId, EndBinId: event.NewMaxBinId, - BinChanges: dlmmBinChangesFromRange(event.NewMinBinId, event.NewMaxBinId, 0), PositionAccount: result.accountList[accounts.positionIdx], }) } @@ -1633,6 +1738,51 @@ func dlmmPositionCloseEventFromInnerInstructions(innerInstructions InnerInstruct return dlmmPositionCloseEvent{}, increaseOffset(offset), false, nil } +func dlmmLbPairCreateEventFromInnerInstructions(innerInstructions InnerInstructions, instruction Instruction, offset [2]uint) (dlmmLbPairCreateEvent, [2]uint, bool, error) { + var prefixLen = offset[1] + inners, err := getInnerInstructions(innerInstructions, prefixLen) + if err != nil { + return dlmmLbPairCreateEvent{}, increaseOffset(offset), false, fmt.Errorf("meteora dlmm create get inner instructions error: %v, offset, %d, %d", err, offset[0], prefixLen) + } + for innerIndex, innerInstr := range inners { + if innerInstr.ProgramIDIndex != instruction.ProgramIDIndex { + continue + } + event, ok := dlmmDecodeLbPairCreateEvent(innerInstr.Data) + if !ok { + continue + } + if offset[1] == 0 { + offset[0] += 1 + } else { + offset[1] = uint(innerIndex) + 1 + prefixLen + } + return event, offset, true, nil + } + return dlmmLbPairCreateEvent{}, increaseOffset(offset), false, nil +} + +func dlmmDecodeLbPairCreateEvent(data []byte) (dlmmLbPairCreateEvent, bool) { + switch { + case len(data) >= 8 && bytes.Equal(data[:8], meteoraInitializeLbPairEventDiscriminator[:]): + var event dlmmLbPairCreateEvent + if err := agbinary.NewBorshDecoder(data[8:]).Decode(&event); err != nil { + return dlmmLbPairCreateEvent{}, false + } + return event, true + case len(data) >= 16 && + bytes.Equal(data[:8], eventDiscriminator[:]) && + bytes.Equal(data[8:16], meteoraInitializeLbPairEventDiscriminator[:]): + var event dlmmLbPairCreateEvent + if err := agbinary.NewBorshDecoder(data[16:]).Decode(&event); err != nil { + return dlmmLbPairCreateEvent{}, false + } + return event, true + default: + return dlmmLbPairCreateEvent{}, false + } +} + func dlmmDecodeAddLiquidityEvent(data []byte) (dlmmAddLiquidityEvent, bool) { switch { case len(data) >= 8 && bytes.Equal(data[:8], meteoraDlmmAddLiquidityEventDiscriminator[:]): @@ -1973,10 +2123,8 @@ func dlmmBuildOneSideAddSwap( tx *Tx, instruction Instruction, addEvent dlmmAddLiquidityEvent, - weightDist []dlmmBinLiquidityDistributionByWeight, startBinId int32, endBinId int32, - hasRange bool, entryContract solana.PublicKey, ) ([]Swap, error) { result := tx.rawTx @@ -1999,13 +2147,6 @@ func dlmmBuildOneSideAddSwap( } } - 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 @@ -2024,7 +2165,6 @@ func dlmmBuildOneSideAddSwap( ActiveBinId: addEvent.ActiveBinId, StartBinId: startBinId, EndBinId: endBinId, - BinChanges: binChanges, PositionAccount: positionAccount, } @@ -2255,56 +2395,50 @@ func dlmmTokenBalanceMeta(result *RawTx, accountIndex int) (TokenBalance, bool) return TokenBalance{}, false } -func dlmmBinChangesFromDistribution(amountX, amountY uint64, dist []dlmmBinLiquidityDistribution) []DlmmBinLiquidityChange { - if len(dist) == 0 { +func dlmmAllocateByWeights(total uint64, weights []uint64) []decimal.Decimal { + if len(weights) == 0 { return nil } - totalX := decimal.NewFromUint64(amountX) - totalY := decimal.NewFromUint64(amountY) - denom := decimal.NewFromInt(10000) - changes := make([]DlmmBinLiquidityChange, 0, len(dist)) - for _, item := range dist { - x := totalX.Mul(decimal.NewFromInt(int64(item.DistributionX))).Div(denom).Truncate(0) - y := totalY.Mul(decimal.NewFromInt(int64(item.DistributionY))).Div(denom).Truncate(0) - changes = append(changes, DlmmBinLiquidityChange{ - BinId: item.BinId, - AmountX: x, - AmountY: y, - }) + + sumWeights := uint64(0) + for _, weight := range weights { + sumWeights += weight } - return changes + if sumWeights == 0 { + sumWeights = uint64(len(weights)) + weights = append([]uint64(nil), weights...) + for i := range weights { + weights[i] = 1 + } + } + + allocations := make([]decimal.Decimal, len(weights)) + remaining := total + for i, weight := range weights { + amount := uint64(0) + if i == len(weights)-1 { + amount = remaining + } else if sumWeights > 0 { + amount = total * weight / sumWeights + if amount > remaining { + amount = remaining + } + remaining -= amount + } + allocations[i] = decimal.NewFromUint64(amount) + } + return allocations } -func dlmmBinChangesFromReduction(reduction []dlmmBinLiquidityReduction) []DlmmBinLiquidityChange { - if len(reduction) == 0 { - return nil +func dlmmApplySignedAllocation(values []decimal.Decimal, negative bool) []decimal.Decimal { + if !negative { + return values } - changes := make([]DlmmBinLiquidityChange, 0, len(reduction)) - for _, item := range reduction { - changes = append(changes, DlmmBinLiquidityChange{ - BinId: item.BinId, - BpsToRemove: item.BpsToRemove, - }) + out := make([]decimal.Decimal, len(values)) + for i, value := range values { + out[i] = value.Neg() } - return changes -} - -func dlmmBinChangesFromRange(startBinId, endBinId int32, bpsToRemove uint16) []DlmmBinLiquidityChange { - if startBinId > endBinId { - startBinId, endBinId = endBinId, startBinId - } - count := int(endBinId-startBinId) + 1 - if count <= 0 { - return nil - } - changes := make([]DlmmBinLiquidityChange, 0, count) - for binId := startBinId; binId <= endBinId; binId++ { - changes = append(changes, DlmmBinLiquidityChange{ - BinId: binId, - BpsToRemove: bpsToRemove, - }) - } - return changes + return out } func dlmmMinMaxBinIDFromCompressedDeposits(bins []dlmmCompressedBinDepositAmount) (startBinID, endBinID int32) { diff --git a/metaoradlmm_test.go b/metaoradlmm_test.go new file mode 100644 index 0000000..bb72d77 --- /dev/null +++ b/metaoradlmm_test.go @@ -0,0 +1,415 @@ +package pump_parser + +import ( + "bytes" + "testing" + + agbinary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" +) + +func testPublicKey(seed byte) solana.PublicKey { + buf := make([]byte, solana.PublicKeyLength) + for i := range buf { + buf[i] = seed + } + return solana.PublicKeyFromBytes(buf) +} + +func seqInts(n int) []int { + out := make([]int, n) + for i := range out { + out[i] = i + } + return out +} + +func mustBorshEncode(t *testing.T, value any) []byte { + t.Helper() + + var buf bytes.Buffer + if err := agbinary.NewBorshEncoder(&buf).Encode(value); err != nil { + t.Fatalf("borsh encode failed: %v", err) + } + return buf.Bytes() +} + +func TestMeteoraDlmmInitializeParserCompatibility(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + discriminator [8]byte + accountCount int + wantPoolPos int + wantBaseMintPos int + wantQuoteMintPos int + wantUserPos int + wantBaseProgramPos int + wantQuoteProgramPos int + }{ + { + name: "initialize_lb_pair", + discriminator: meteoraInitializeLbPairDiscriminator, + accountCount: 14, + wantPoolPos: 0, + wantBaseMintPos: 2, + wantQuoteMintPos: 3, + wantUserPos: 8, + wantBaseProgramPos: 9, + wantQuoteProgramPos: 9, + }, + { + name: "initialize_lb_pair2", + discriminator: meteoraInitializeLbPair2Discriminator, + accountCount: 16, + wantPoolPos: 0, + wantBaseMintPos: 2, + wantQuoteMintPos: 3, + wantUserPos: 8, + wantBaseProgramPos: 11, + wantQuoteProgramPos: 12, + }, + { + name: "initialize_customizable_permissionless_lb_pair", + discriminator: meteoraInitializeCustomizablePermissionlessLbPairDiscriminator, + accountCount: 14, + wantPoolPos: 0, + wantBaseMintPos: 2, + wantQuoteMintPos: 3, + wantUserPos: 8, + wantBaseProgramPos: 9, + wantQuoteProgramPos: 9, + }, + { + name: "initialize_customizable_permissionless_lb_pair2", + discriminator: meteoraInitializeCustomizablePermissionlessLbPair2Discriminator, + accountCount: 17, + wantPoolPos: 0, + wantBaseMintPos: 2, + wantQuoteMintPos: 3, + wantUserPos: 8, + wantBaseProgramPos: 11, + wantQuoteProgramPos: 12, + }, + { + name: "initialize_permission_lb_pair", + discriminator: meteoraInitializePermissionLbPairDiscriminator, + accountCount: 17, + wantPoolPos: 1, + wantBaseMintPos: 3, + wantQuoteMintPos: 4, + wantUserPos: 8, + wantBaseProgramPos: 11, + wantQuoteProgramPos: 12, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + accountList := make([]solana.PublicKey, 32) + for i := range accountList { + accountList[i] = testPublicKey(byte(i + 1)) + } + programIndex := 30 + accountList[programIndex] = meteoraDlmmProgram + + instruction := Instruction{ + Accounts: seqInts(tc.accountCount), + Data: solana.Base58(tc.discriminator[:]), + ProgramIDIndex: programIndex, + } + + rawTx := &RawTx{ + accountList: accountList, + Meta: Meta{ + PostTokenBalances: []TokenBalance{ + { + MintAccount: accountList[tc.wantBaseMintPos], + UITokenAmount: UITokenAmount{ + Decimals: 6, + }, + }, + { + MintAccount: accountList[tc.wantQuoteMintPos], + UITokenAmount: UITokenAmount{ + Decimals: 9, + }, + }, + }, + }, + Transaction: Transaction{ + Message: Message{ + Instructions: []Instruction{instruction}, + }, + }, + } + + tx := &Tx{rawTx: rawTx} + + swaps, _, err := metaoradlmmParser(tx, instruction, InnerInstructions{}, [2]uint{0, 0}) + if err != nil { + t.Fatalf("metaoradlmmParser() error = %v", err) + } + if len(swaps) != 1 { + t.Fatalf("metaoradlmmParser() swaps len = %d, want 1", len(swaps)) + } + + swap := swaps[0] + if !swap.Pool.Equals(accountList[tc.wantPoolPos]) { + t.Fatalf("swap.Pool = %s, want %s", swap.Pool, accountList[tc.wantPoolPos]) + } + if !swap.BaseMint.Equals(accountList[tc.wantBaseMintPos]) { + t.Fatalf("swap.BaseMint = %s, want %s", swap.BaseMint, accountList[tc.wantBaseMintPos]) + } + if !swap.QuoteMint.Equals(accountList[tc.wantQuoteMintPos]) { + t.Fatalf("swap.QuoteMint = %s, want %s", swap.QuoteMint, accountList[tc.wantQuoteMintPos]) + } + if !swap.User.Equals(accountList[tc.wantUserPos]) { + t.Fatalf("swap.User = %s, want %s", swap.User, accountList[tc.wantUserPos]) + } + if !swap.BaseTokenProgram.Equals(accountList[tc.wantBaseProgramPos]) { + t.Fatalf("swap.BaseTokenProgram = %s, want %s", swap.BaseTokenProgram, accountList[tc.wantBaseProgramPos]) + } + if !swap.QuoteTokenProgram.Equals(accountList[tc.wantQuoteProgramPos]) { + t.Fatalf("swap.QuoteTokenProgram = %s, want %s", swap.QuoteTokenProgram, accountList[tc.wantQuoteProgramPos]) + } + if swap.BaseMintDecimals != 6 { + t.Fatalf("swap.BaseMintDecimals = %d, want 6", swap.BaseMintDecimals) + } + if swap.QuoteMintDecimals != 9 { + t.Fatalf("swap.QuoteMintDecimals = %d, want 9", swap.QuoteMintDecimals) + } + if !swap.EntryContract.Equals(meteoraDlmmProgram) { + t.Fatalf("swap.EntryContract = %s, want %s", swap.EntryContract, meteoraDlmmProgram) + } + }) + } +} + +func TestDlmmDecodeLbPairCreateEvent(t *testing.T) { + t.Parallel() + + event := dlmmLbPairCreateEvent{ + LbPair: testPublicKey(90), + BinStep: 42, + TokenX: testPublicKey(91), + TokenY: testPublicKey(92), + } + + body := mustBorshEncode(t, event) + + barePayload := append(append([]byte{}, meteoraInitializeLbPairEventDiscriminator[:]...), body...) + decodedBare, ok := dlmmDecodeLbPairCreateEvent(barePayload) + if !ok { + t.Fatalf("dlmmDecodeLbPairCreateEvent() failed for bare payload") + } + if decodedBare != event { + t.Fatalf("decoded bare event = %+v, want %+v", decodedBare, event) + } + + anchorPayload := append(append(append([]byte{}, eventDiscriminator[:]...), meteoraInitializeLbPairEventDiscriminator[:]...), body...) + decodedAnchor, ok := dlmmDecodeLbPairCreateEvent(anchorPayload) + if !ok { + t.Fatalf("dlmmDecodeLbPairCreateEvent() failed for anchor payload") + } + if decodedAnchor != event { + t.Fatalf("decoded anchor event = %+v, want %+v", decodedAnchor, event) + } +} + +func TestMeteoraDlmmInitializeParserUsesLbPairCreateEvent(t *testing.T) { + t.Parallel() + + accountList := make([]solana.PublicKey, 32) + for i := range accountList { + accountList[i] = testPublicKey(byte(i + 1)) + } + programIndex := 30 + accountList[programIndex] = meteoraDlmmProgram + + instruction := Instruction{ + Accounts: seqInts(16), + Data: solana.Base58(meteoraInitializeLbPair2Discriminator[:]), + ProgramIDIndex: programIndex, + } + + event := dlmmLbPairCreateEvent{ + LbPair: testPublicKey(111), + BinStep: 25, + TokenX: testPublicKey(112), + TokenY: testPublicKey(113), + } + innerEventData := append( + append(append([]byte{}, eventDiscriminator[:]...), meteoraInitializeLbPairEventDiscriminator[:]...), + mustBorshEncode(t, event)..., + ) + + rawTx := &RawTx{ + accountList: accountList, + Meta: Meta{ + PostTokenBalances: []TokenBalance{ + { + MintAccount: accountList[2], + UITokenAmount: UITokenAmount{ + Decimals: 6, + }, + }, + { + MintAccount: accountList[3], + UITokenAmount: UITokenAmount{ + Decimals: 9, + }, + }, + }, + InnerInstructions: []InnerInstructions{ + { + Index: 0, + Instructions: []Instruction{ + { + ProgramIDIndex: programIndex, + Data: solana.Base58(innerEventData), + }, + }, + }, + }, + }, + Transaction: Transaction{ + Message: Message{ + Instructions: []Instruction{instruction}, + }, + }, + } + + tx := &Tx{rawTx: rawTx} + + swaps, nextOffset, err := metaoradlmmParser(tx, instruction, rawTx.Meta.InnerInstructions[0], [2]uint{0, 0}) + if err != nil { + t.Fatalf("metaoradlmmParser() error = %v", err) + } + if len(swaps) != 1 { + t.Fatalf("metaoradlmmParser() swaps len = %d, want 1", len(swaps)) + } + + swap := swaps[0] + if !swap.Pool.Equals(event.LbPair) { + t.Fatalf("swap.Pool = %s, want event %s", swap.Pool, event.LbPair) + } + if !swap.BaseMint.Equals(event.TokenX) { + t.Fatalf("swap.BaseMint = %s, want event %s", swap.BaseMint, event.TokenX) + } + if !swap.QuoteMint.Equals(event.TokenY) { + t.Fatalf("swap.QuoteMint = %s, want event %s", swap.QuoteMint, event.TokenY) + } + if nextOffset != ([2]uint{1, 0}) { + t.Fatalf("nextOffset = %#v, want [2]uint{1, 0}", nextOffset) + } +} + +func TestDlmmSwapFeeInfo(t *testing.T) { + t.Parallel() + + baseMint := testPublicKey(1) + quoteMint := testPublicKey(2) + baseProgram := testPublicKey(3) + quoteProgram := testPublicKey(4) + + testCases := []struct { + name string + baseIsX bool + swapForY bool + wantFeeSide string + wantFeeMint solana.PublicKey + wantFeeProg solana.PublicKey + wantDecimals uint8 + }{ + { + name: "x is base and input is x", + baseIsX: true, + swapForY: true, + wantFeeSide: "base", + wantFeeMint: baseMint, + wantFeeProg: baseProgram, + wantDecimals: 6, + }, + { + name: "x is base and input is y", + baseIsX: true, + swapForY: false, + wantFeeSide: "quote", + wantFeeMint: quoteMint, + wantFeeProg: quoteProgram, + wantDecimals: 9, + }, + { + name: "y is base and input is x", + baseIsX: false, + swapForY: true, + wantFeeSide: "quote", + wantFeeMint: quoteMint, + wantFeeProg: quoteProgram, + wantDecimals: 9, + }, + { + name: "y is base and input is y", + baseIsX: false, + swapForY: false, + wantFeeSide: "base", + wantFeeMint: baseMint, + wantFeeProg: baseProgram, + wantDecimals: 6, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + feeAmount, feeSide, feeMint, feeProgram, feeDecimals := dlmmSwapFeeInfo( + tc.baseIsX, + tc.swapForY, + 123, + baseMint, + quoteMint, + baseProgram, + quoteProgram, + 6, + 9, + ) + if !feeAmount.Equal(decimal.NewFromInt(123)) { + t.Fatalf("feeAmount = %s, want 123", feeAmount) + } + if feeSide != tc.wantFeeSide { + t.Fatalf("feeSide = %s, want %s", feeSide, tc.wantFeeSide) + } + if !feeMint.Equals(tc.wantFeeMint) { + t.Fatalf("feeMint = %s, want %s", feeMint, tc.wantFeeMint) + } + if !feeProgram.Equals(tc.wantFeeProg) { + t.Fatalf("feeProgram = %s, want %s", feeProgram, tc.wantFeeProg) + } + if feeDecimals != tc.wantDecimals { + t.Fatalf("feeDecimals = %d, want %d", feeDecimals, tc.wantDecimals) + } + }) + } +} + +func TestDlmmSwapLpFeeAmount(t *testing.T) { + t.Parallel() + + lpFee := dlmmSwapLpFeeAmount(100, 15, 5) + if !lpFee.Equal(decimal.NewFromInt(80)) { + t.Fatalf("lpFee = %s, want 80", lpFee) + } + + lpFee = dlmmSwapLpFeeAmount(10, 8, 5) + if !lpFee.IsZero() { + t.Fatalf("lpFee should floor at zero, got %s", lpFee) + } +} diff --git a/tx.go b/tx.go index f0c3dde..4351903 100644 --- a/tx.go +++ b/tx.go @@ -27,9 +27,15 @@ type Swap struct { BaseMintDecimals uint8 QuoteMintDecimals uint8 - User solana.PublicKey - BaseAmount decimal.Decimal - QuoteAmount decimal.Decimal + User solana.PublicKey + BaseAmount decimal.Decimal + QuoteAmount decimal.Decimal + FeeAmount decimal.Decimal + LpFeeAmount decimal.Decimal + FeeSide string + FeeMint solana.PublicKey + FeeTokenProgram solana.PublicKey + FeeMintDecimals uint8 BaseReserve decimal.Decimal QuoteReserve decimal.Decimal @@ -48,23 +54,15 @@ type Swap struct { AfterSOLBalance decimal.Decimal //For meteora dlmm - ActiveBinId int32 - StartBinId int32 - EndBinId int32 - RemoveBp int32 - BinChanges []DlmmBinLiquidityChange + ActiveBinId int32 + StartBinId int32 + EndBinId int32 + RemoveBp int32 PositionAccount solana.PublicKey ConsumeUnit uint64 } -type DlmmBinLiquidityChange struct { - BinId int32 - AmountX decimal.Decimal - AmountY decimal.Decimal - BpsToRemove uint16 -} - type platformInfo struct { Platform string PlatformFee decimal.Decimal