diff --git a/checking.go b/checking.go deleted file mode 100644 index 04b83fd..0000000 --- a/checking.go +++ /dev/null @@ -1,576 +0,0 @@ -package pump_parser - -import ( - "log" - "strings" - - "github.com/gagliardetto/solana-go" -) - -func checkBonkGmgnBuy(rawTx *RawTx) bool { - - // 检查交易版本 - var version, _ = rawTx.Version.(solana.MessageVersion) - if version != solana.MessageVersionLegacy { - return false - } - - // 检查交易指令数量 - if len(rawTx.Transaction.Message.Instructions) != 10 && len(rawTx.Transaction.Message.Instructions) != 9 { - return false - } - accountList := rawTx.getAccountList() - // 检查 cu limit - { - instruction := rawTx.Transaction.Message.Instructions[0] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.ComputeBudget { - return false - } - - if len(instruction.Accounts) != 1 { - return false - } - - accountId := accountList[instruction.Accounts[0]].String() - if !strings.HasPrefix(accountId, "jitodontfront1111111111151111111111111655") { - return false - } - } - - // 检查 cu price - { - instruction := rawTx.Transaction.Message.Instructions[1] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.ComputeBudget { - return false - } - } - - // 检查 ata.createIdempotent - { - instruction := rawTx.Transaction.Message.Instructions[2] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SPLAssociatedTokenAccountProgramID { - return false - } - - if instruction.Data.String() != "2" { - return false - } - - if len(instruction.Accounts) < 4 { - return false - } - - // gmgn 会先创建 wsol 账户, 而不是 token 账户 - accountId := accountList[instruction.Accounts[3]] - if accountId != solana.WrappedSol { - return false - } - } - - // 检查 transfer - { - instruction := rawTx.Transaction.Message.Instructions[3] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SystemProgramID { - return false - } - - if len(instruction.Data) == 0 || instruction.Data[0] != 2 { - return false - } - } - - // 检查 token.syncNative - { - instruction := rawTx.Transaction.Message.Instructions[4] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.TokenProgramID { - return false - } - - if instruction.Data.String() != "J" { - return false - } - } - - offset := 5 - if len(rawTx.Transaction.Message.Instructions) == 10 { - // 检查 ata.create - { - instruction := rawTx.Transaction.Message.Instructions[offset] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SPLAssociatedTokenAccountProgramID { - return false - } - - if instruction.Data.String() != "1" { - return false - } - } - - offset++ - } - - // 检查 bonk.buy - { - instruction := rawTx.Transaction.Message.Instructions[offset] - programId := accountList[instruction.ProgramIDIndex] - if programId != raydiumLaunchLabProgramID { - return false - } - } - - offset++ - - // 检查 token.closeAccount - { - instruction := rawTx.Transaction.Message.Instructions[offset] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.TokenProgramID { - return false - } - - if instruction.Data.String() != "A" { - return false - } - } - - offset++ - - // 检查 transfer - { - instruction := rawTx.Transaction.Message.Instructions[offset] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SystemProgramID { - return false - } - - if len(instruction.Data) == 0 || instruction.Data[0] != 2 { - return false - } - } - - offset++ - - // 检查 transfer - { - instruction := rawTx.Transaction.Message.Instructions[offset] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SystemProgramID { - return false - } - - if len(instruction.Data) == 0 || instruction.Data[0] != 2 { - return false - } - } - - return true -} - -var ( - axiomTxLoopupTable = solana.MustPublicKeyFromBase58("7RKtfATWCe98ChuwecNq8XCzAzfoK3DtZTprFsPMGtio") - axiomProgramID = solana.MustPublicKeyFromBase58("AxiomfHaWDemCFBLBayqnEnNwE6b7B2Qz3UmzMpgbMG6") - gmgnProgramID = solana.MustPublicKeyFromBase58("GMgnVFR8Jb39LoXsEVzb3DvBy3ywCmdmJquHUy1Lrkqb") -) - -func checkBonkAxiomBuy(rawTx *RawTx) bool { - - // 检查交易版本 - var version, _ = rawTx.Version.(solana.MessageVersion) - if version == solana.MessageVersionLegacy || len(rawTx.Transaction.Message.AddressTableLookups) != 1 { - log.Printf("CheckBonkAxiomBuy: Invalid transaction version or address table lookups: %v %v %v", rawTx.Transaction.Signatures[0].String(), version, len(rawTx.Transaction.Message.AddressTableLookups)) - return false - } - - // 检查 addressLookupTable 是否是 Axiom 的 - if rawTx.Transaction.Message.AddressTableLookups[0].AccountKey != axiomTxLoopupTable { - log.Printf("CheckBonkAxiomBuy: Invalid address lookup table: %v", rawTx.Transaction.Signatures[0].String()) - return false - } - - // 检查交易指令数量 - if len(rawTx.Transaction.Message.Instructions) != 10 { - return false - } - accountList := rawTx.getAccountList() - // 检查 cu limit - { - instruction := rawTx.Transaction.Message.Instructions[0] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.ComputeBudget { - log.Printf("CheckBonkAxiomBuy: Invalid program ID for ComputeBudget: %v", programId) - return false - } - - if len(instruction.Accounts) != 1 { - log.Printf("CheckBonkAxiomBuy: Invalid number of accounts for ComputeBudget: %v", len(instruction.Accounts)) - return false - } - - accountId := accountList[instruction.Accounts[0]].String() - if !strings.HasPrefix(accountId, "jitodontfront") || !strings.HasSuffix(accountId, "TradeWithAxiomDotTrade") { - log.Printf("CheckBonkAxiomBuy: Invalid account ID for ComputeBudget: %v", accountId) - return false - } - } - - // 检查 cu price - { - instruction := rawTx.Transaction.Message.Instructions[1] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.ComputeBudget { - log.Printf("CheckBonkAxiomBuy: Invalid program ID for ComputeBudget: %v", programId) - return false - } - } - - // 检查 ata.createIdempotent - { - instruction := rawTx.Transaction.Message.Instructions[2] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SPLAssociatedTokenAccountProgramID { - log.Printf("CheckBonkAxiomBuy: Invalid program ID for SPLAssociatedTokenAccount: %v", programId) - return false - } - - if instruction.Data.String() != "2" { - log.Printf("CheckBonkAxiomBuy: Invalid data for SPLAssociatedTokenAccount: %v", instruction.Data.String()) - return false - } - - if len(instruction.Accounts) < 4 { - log.Printf("CheckBonkAxiomBuy: Invalid number of accounts for SPLAssociatedTokenAccount: %v", len(instruction.Accounts)) - return false - } - - // axiom 会先创建 token 账户, 而不是 wsol 账户 - accountId := accountList[instruction.Accounts[3]] - if accountId == solana.WrappedSol { - log.Printf("CheckBonkAxiomBuy: Invalid account ID for SPLAssociatedTokenAccount, expected token account but got wsol: %v", accountId) - return false - } - } - - // 检查 ata.createIdempotent - { - instruction := rawTx.Transaction.Message.Instructions[3] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SPLAssociatedTokenAccountProgramID { - log.Printf("CheckBonkAxiomBuy: Invalid program ID for SPLAssociatedTokenAccount2: %v", programId) - return false - } - - if instruction.Data.String() != "2" { - log.Printf("CheckBonkAxiomBuy: Invalid data for SPLAssociatedTokenAccount2: %v", instruction.Data.String()) - return false - } - - if len(instruction.Accounts) < 4 { - log.Printf("CheckBonkAxiomBuy: Invalid number of accounts for SPLAssociatedTokenAccount2: %v", len(instruction.Accounts)) - return false - } - - // axiom 会先创建 token 账户, 而不是 wsol 账户 - accountId := accountList[instruction.Accounts[3]] - if accountId != solana.WrappedSol { - log.Printf("CheckBonkAxiomBuy: Invalid account ID for SPLAssociatedTokenAccount2, expected wsol but got: %v", accountId) - return false - } - } - - // 检查 transfer - { - instruction := rawTx.Transaction.Message.Instructions[4] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SystemProgramID { - log.Printf("CheckBonkAxiomBuy: Invalid program ID for SystemProgram3: %v", programId) - return false - } - - if len(instruction.Data) == 0 || instruction.Data[0] != 2 { - log.Printf("CheckBonkAxiomBuy: Invalid data for SystemProgram transfer3: %v", instruction.Data) - return false - } - } - - // 检查 token.syncNative - { - instruction := rawTx.Transaction.Message.Instructions[5] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.TokenProgramID { - log.Printf("CheckBonkAxiomBuy: Invalid program ID for TokenProgram3: %v", programId) - return false - } - - if instruction.Data.String() != "J" { - log.Printf("CheckBonkAxiomBuy: Invalid data for TokenProgram syncNative3: %v", instruction.Data.String()) - return false - } - } - - // 检查 bonk.buy - { - instruction := rawTx.Transaction.Message.Instructions[6] - programId := accountList[instruction.ProgramIDIndex] - if programId != raydiumLaunchLabProgramID { - return false - } - } - - // 检查 token.closeAccount - { - instruction := rawTx.Transaction.Message.Instructions[7] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.TokenProgramID { - log.Printf("CheckBonkAxiomBuy: Invalid program ID for TokenProgram closeAccount: %v", programId) - return false - } - - if instruction.Data.String() != "A" { - log.Printf("CheckBonkAxiomBuy: Invalid data for TokenProgram closeAccount: %v", instruction.Data.String()) - return false - } - } - - // 检查 transfer - { - instruction := rawTx.Transaction.Message.Instructions[8] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SystemProgramID { - log.Printf("CheckBonkAxiomBuy: Invalid program ID for SystemProgram transfer4: %v", programId) - return false - } - - if len(instruction.Data) == 0 || instruction.Data[0] != 2 { - log.Printf("CheckBonkAxiomBuy: Invalid data for SystemProgram transfer4: %v", instruction.Data) - return false - } - } - - // 检查 transfer - { - instruction := rawTx.Transaction.Message.Instructions[9] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SystemProgramID { - log.Printf("CheckBonkAxiomBuy: Invalid program ID for SystemProgram transfer5: %v", programId) - return false - } - - if len(instruction.Data) == 0 || instruction.Data[0] != 2 { - log.Printf("CheckBonkAxiomBuy: Invalid data for SystemProgram transfer5: %v", instruction.Data) - return false - } - } - - return true -} - -func checkPumpFunAxiomBuy(rawTx *RawTx) bool { - - // 检查交易版本 - if rawTx.Version == "legacy" || len(rawTx.Transaction.Message.AddressTableLookups) != 1 { - return false - } - - // 检查 addressLookupTable 是否是 Axiom 的 - if rawTx.Transaction.Message.AddressTableLookups[0].AccountKey != axiomTxLoopupTable { - return false - } - - // 检查交易指令数量 - if len(rawTx.Transaction.Message.Instructions) != 6 { - return false - } - - accountList := rawTx.getAccountList() - // 检查 cu limit - { - instruction := rawTx.Transaction.Message.Instructions[0] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.ComputeBudget { - return false - } - - if len(instruction.Accounts) != 1 { - return false - } - - accountId := accountList[instruction.Accounts[0]].String() - if !strings.HasPrefix(accountId, "jitodontfront") || !strings.HasSuffix(accountId, "TradeWithAxiomDotTrade") { - return false - } - } - - // 检查 cu price - { - instruction := rawTx.Transaction.Message.Instructions[1] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.ComputeBudget { - return false - } - } - - // 检查 ata.createIdempotent - { - instruction := rawTx.Transaction.Message.Instructions[2] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SPLAssociatedTokenAccountProgramID { - return false - } - - if instruction.Data.String() != "2" { - return false - } - - if len(instruction.Accounts) < 4 { - return false - } - - // axiom 会先创建 token 账户, 而不是 wsol 账户 - accountId := accountList[instruction.Accounts[3]] - if accountId == solana.WrappedSol { - return false - } - } - - // 检查调用axiom合约 - { - instruction := rawTx.Transaction.Message.Instructions[3] - programId := accountList[instruction.ProgramIDIndex] - if programId != axiomProgramID { - return false - } - } - - // 检查 transfer - { - instruction := rawTx.Transaction.Message.Instructions[4] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SystemProgramID { - return false - } - - if len(instruction.Data) == 0 || instruction.Data[0] != 2 { - return false - } - } - - // 检查 transfer - { - instruction := rawTx.Transaction.Message.Instructions[5] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SystemProgramID { - return false - } - - if len(instruction.Data) == 0 || instruction.Data[0] != 2 { - return false - } - } - - return true -} - -func checkPumpFunGmgnBuy(rawTx *RawTx) bool { - - // 检查交易版本 - if rawTx.Version != "legacy" { - return false - } - - // 检查交易指令数量 - if len(rawTx.Transaction.Message.Instructions) != 6 { - return false - } - - accountList := rawTx.getAccountList() - // 检查 cu limit - { - instruction := rawTx.Transaction.Message.Instructions[0] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.ComputeBudget { - return false - } - - if len(instruction.Accounts) != 1 { - return false - } - - accountId := accountList[instruction.Accounts[0]].String() - if !strings.HasPrefix(accountId, "jitodontfront1111111111151111111111111655") { - return false - } - } - - // 检查 cu price - { - instruction := rawTx.Transaction.Message.Instructions[1] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.ComputeBudget { - return false - } - } - - // 检查 ata.createIdempotent - { - instruction := rawTx.Transaction.Message.Instructions[2] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SPLAssociatedTokenAccountProgramID { - return false - } - - if instruction.Data.String() != "2" { - return false - } - - if len(instruction.Accounts) < 4 { - return false - } - - accountId := accountList[instruction.Accounts[3]] - if accountId == solana.WrappedSol { - return false - } - } - - // 检查调用 gmgn 合约 - { - instruction := rawTx.Transaction.Message.Instructions[3] - programId := accountList[instruction.ProgramIDIndex] - if programId != gmgnProgramID { - return false - } - } - - // 检查 transfer - { - instruction := rawTx.Transaction.Message.Instructions[4] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SystemProgramID { - return false - } - - if len(instruction.Data) == 0 || instruction.Data[0] != 2 { - return false - } - } - - // 检查 transfer - { - instruction := rawTx.Transaction.Message.Instructions[5] - programId := accountList[instruction.ProgramIDIndex] - if programId != solana.SystemProgramID { - return false - } - - if len(instruction.Data) == 0 || instruction.Data[0] != 2 { - return false - } - } - - return true -} diff --git a/error.go b/error.go index 97d5498..8ca9aa3 100644 --- a/error.go +++ b/error.go @@ -79,54 +79,54 @@ const ( const ( GenericError InstructionErrorVariant = iota - /// The arguments provided to a program were invalid + // InvalidArgument / The arguments provided to a program were invalid InvalidArgument - /// An instruction's data contents were invalid + // InvalidInstructionData / An instruction's data contents were invalid InvalidInstructionData - /// An account's data contents was invalid + // InvalidAccountData / An account's data contents was invalid InvalidAccountData - /// An account's data was too small + // AccountDataTooSmall / An account's data was too small AccountDataTooSmall - /// An account's balance was too small to complete the instruction + // InsufficientFunds / An account's balance was too small to complete the instruction InsufficientFunds - /// The account did not have the expected program id + // IncorrectProgramId / The account did not have the expected program id IncorrectProgramId - /// A signature was required but not found + // MissingRequiredSignature / A signature was required but not found MissingRequiredSignature - /// An initialize instruction was sent to an account that has already been initialized. + // AccountAlreadyInitialized / An initialize instruction was sent to an account that has already been initialized. AccountAlreadyInitialized - /// An attempt to operate on an account that hasn't been initialized. + // UninitializedAccount / An attempt to operate on an account that hasn't been initialized. UninitializedAccount - /// Program's instruction lamport balance does not equal the balance after the instruction + // UnbalancedInstruction / Program's instruction lamport balance does not equal the balance after the instruction UnbalancedInstruction - /// Program illegally modified an account's program id + // ModifiedProgramId / Program illegally modified an account's program id ModifiedProgramId - /// Program spent the lamports of an account that doesn't belong to it + // ExternalAccountLamportSpend / Program spent the lamports of an account that doesn't belong to it ExternalAccountLamportSpend - /// Program modified the data of an account that doesn't belong to it + // ExternalAccountDataModified / Program modified the data of an account that doesn't belong to it ExternalAccountDataModified - /// Read-only account's lamports modified + // ReadonlyLamportChange / Read-only account's lamports modified ReadonlyLamportChange - /// Read-only account's data was modified + // ReadonlyDataModified / Read-only account's data was modified ReadonlyDataModified - /// An account was referenced more than once in a single instruction + // DuplicateAccountIndex / An account was referenced more than once in a single instruction // Deprecated, instructions can now contain duplicate accounts DuplicateAccountIndex - /// Executable bit on account changed, but shouldn't have + // ExecutableModified / Executable bit on account changed, but shouldn't have ExecutableModified - /// Rent_epoch account changed, but shouldn't have + // RentEpochModified / Rent_epoch account changed, but shouldn't have RentEpochModified - /// The instruction expected additional account keys + // NotEnoughAccountKeys / The instruction expected additional account keys NotEnoughAccountKeys - /// Program other than the account's owner changed the size of the account data + // AccountDataSizeChanged / Program other than the account's owner changed the size of the account data AccountDataSizeChanged - /// The instruction expected an executable account + // AccountNotExecutable / The instruction expected an executable account AccountNotExecutable - /// Failed to borrow a reference to account data, already borrowed + // AccountBorrowFailed / Failed to borrow a reference to account data, already borrowed AccountBorrowFailed - /// Account data has an outstanding reference after a program's execution + // InstructionAccountBorrowOutstanding / Account data has an outstanding reference after a program's execution InstructionAccountBorrowOutstanding - /// The same account was multiply passed to an on-chain program's entrypoint, but the program + // DuplicateAccountOutOfSync / The same account was multiply passed to an on-chain program's entrypoint, but the program /// modified them differently. A program can only modify one instance of the account because /// the runtime cannot determine which changes to pick or how to merge them if both are modified DuplicateAccountOutOfSync @@ -136,42 +136,42 @@ const ( Custom // Custom(u32), - /// The return value from the program was invalid. Valid errors are either a defined builtin + // InvalidError / The return value from the program was invalid. Valid errors are either a defined builtin /// error value or a user-defined error in the lower 32 bits. InvalidError - /// Executable account's data was modified + // ExecutableDataModified / Executable account's data was modified ExecutableDataModified - /// Executable account's lamports modified + // ExecutableLamportChange / Executable account's lamports modified ExecutableLamportChange - /// Executable accounts must be rent exempt + // ExecutableAccountNotRentExempt / Executable accounts must be rent exempt ExecutableAccountNotRentExempt - /// Unsupported program id + // UnsupportedProgramId / Unsupported program id UnsupportedProgramId - /// Cross-program invocation call depth too deep + // CallDepth / Cross-program invocation call depth too deep CallDepth - /// An account required by the instruction is missing + // MissingAccount / An account required by the instruction is missing MissingAccount - /// Cross-program invocation reentrancy not allowed for this instruction + // ReentrancyNotAllowed / Cross-program invocation reentrancy not allowed for this instruction ReentrancyNotAllowed - /// Length of the seed is too long for address generation + // MaxSeedLengthExceeded / Length of the seed is too long for address generation MaxSeedLengthExceeded - /// Provided seeds do not result in a valid address + // InvalidSeeds / Provided seeds do not result in a valid address InvalidSeeds - /// Failed to reallocate account data of this length + // InvalidRealloc / Failed to reallocate account data of this length InvalidRealloc - /// Computational budget exceeded + // ComputationalBudgetExceeded / Computational budget exceeded ComputationalBudgetExceeded - /// Cross-program invocation with unauthorized signer or writable account + // PrivilegeEscalation / Cross-program invocation with unauthorized signer or writable account PrivilegeEscalation - /// Failed to create program execution environment + // ProgramEnvironmentSetupFailure / Failed to create program execution environment ProgramEnvironmentSetupFailure - /// Program failed to complete + // ProgramFailedToComplete / Program failed to complete ProgramFailedToComplete - /// Program failed to compile + // ProgramFailedToCompile / Program failed to compile ProgramFailedToCompile - /// Account is immutable + // Immutable / Account is immutable Immutable - /// Incorrect authority provided + // IncorrectAuthority / Incorrect authority provided IncorrectAuthority /// Failed to serialize or deserialize account data /// @@ -185,23 +185,23 @@ const ( BorshIoError // BorshIoError(String) - // An account does not have enough lamports to be rent-exempt + // AccountNotRentExempt An account does not have enough lamports to be rent-exempt AccountNotRentExempt - /// Invalid account owner + // InvalidAccountOwner Invalid account owner InvalidAccountOwner - /// Program arithmetic overflowed + // ArithmeticOverflow Program arithmetic overflowed ArithmeticOverflow - /// Unsupported sysvar + // UnsupportedSysvar Unsupported sysvar UnsupportedSysvar - /// Illegal account owner + // IllegalOwner Illegal account owner IllegalOwner - /// Accounts data allocations exceeded the maximum allowed per transaction + // MaxAccountsDataAllocationsExceeded / Accounts data allocations exceeded the maximum allowed per transaction MaxAccountsDataAllocationsExceeded - /// Max accounts exceeded + // MaxAccountsExceeded Max accounts exceeded MaxAccountsExceeded - /// Max instruction trace length exceeded + // MaxInstructionTraceLengthExceeded Max instruction trace length exceeded MaxInstructionTraceLengthExceeded - /// Builtin programs must consume compute units + // BuiltinProgramsMustConsumeComputeUnits Builtin programs must consume compute units BuiltinProgramsMustConsumeComputeUnits ) @@ -210,6 +210,15 @@ type TransactionError struct { rest []byte } +type TransactionParsedError struct { + Index uint8 + Variant TransactionErrorVariant + Enum InstructionErrorVariant + CustomCode uint32 + + UnKnown string +} + var ( ErrInvalidTransactionError = errors.New("invalid transaction error") NotAnInstructionError = errors.New("not an instruction error") @@ -233,6 +242,49 @@ func DecodeTransactionError(data []byte) (*TransactionError, error) { return &err, nil } +func ParseTransactionErrorFromGeyser(data []byte) *TransactionParsedError { + if len(data) == 0 { + return nil + } + transactionError, err := DecodeTransactionError(data) + if err != nil { + return &TransactionParsedError{ + UnKnown: string(data), + } + } + + enumErr, err := transactionError.GetInstructionError() + if err != nil { + return &TransactionParsedError{ + Variant: transactionError.Variant, + UnKnown: string(data), + } + } + if enumErr.Variant != Custom { + return &TransactionParsedError{ + Index: enumErr.Index, + Variant: transactionError.Variant, + Enum: enumErr.Variant, + } + } + customCode, err := enumErr.Custom() + if err != nil { + return &TransactionParsedError{ + Index: enumErr.Index, + Variant: transactionError.Variant, + Enum: enumErr.Variant, + UnKnown: string(data), + } + } + + return &TransactionParsedError{ + Index: enumErr.Index, + Variant: transactionError.Variant, + Enum: enumErr.Variant, + CustomCode: customCode.Code, + } +} + func (e *TransactionError) GetCustomErrorCode() (uint8, uint32, error) { instr, err := e.GetInstructionError() if err != nil { diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..0b593dd --- /dev/null +++ b/error_test.go @@ -0,0 +1,47 @@ +package pump_parser + +import ( + "encoding/base64" + "testing" +) + +func TestDecodeTransactionError(t *testing.T) { + var testCases = []struct { + name string + data string + }{ + { + name: "ComputationalBudgetExceeded", + data: "CAAAAAMlAAAA", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bytesData, err := base64.StdEncoding.DecodeString(tc.data) + if err != nil { + t.Fatalf("Failed to decode base64 data for %s: %v", tc.name, err) + return + } + te, err := DecodeTransactionError(bytesData) + if err != nil { + t.Errorf("Decoded error for %s: %v", tc.name, err) + return + } + + if te.Variant != InstructionError { + t.Errorf("Expected Variant to be InstructionError for %s, got %d", tc.name, te.Variant) + return + } + errName, err := te.GetInstructionError() + if err != nil { + t.Errorf("Failed to get instruction error for %s: %v", tc.name, err) + return + } + if errName.Variant != ComputationalBudgetExceeded { + t.Errorf("Expected instruction error variant to be Custom for %s, got %d", tc.name, errName.Variant) + return + } + }) + } +} diff --git a/internal/example/cmd/main.go b/internal/example/cmd/main.go index 8c78bdf..7f82dd4 100644 --- a/internal/example/cmd/main.go +++ b/internal/example/cmd/main.go @@ -5,10 +5,17 @@ import ( "fmt" "time" + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" parser "github.com/thloyi/pump-parser" example "github.com/thloyi/pump-parser/internal/example" ) +var ( + pumpProgram = solana.MustPublicKeyFromBase58("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") + pumpAmmProgram = solana.MustPublicKeyFromBase58("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") +) + func main() { //pool, err := ants.NewPool(100, ants.WithPreAlloc(true), ants.WithNonblocking(true)) //if err != nil { @@ -31,7 +38,7 @@ func main() { continue } ptx := msg.Tx - fmt.Println("consume", ptx.ComputeUnitsConsumed, "limit", ptx.CuLimit, "hash", ptx.GetTxHash()) + // fmt.Println("consume", ptx.ComputeUnitsConsumed, "limit", ptx.CuLimit, "hash", ptx.GetTxHash()) //data, _ := json.Marshal(tx) //fmt.Println(string(data)) //continue @@ -44,40 +51,9 @@ func main() { //} // 处理交易 - txErr, ok := ptx.Err.(*parser.TransactionError) - var customerErrCode uint32 - var instructorErrIndex uint8 - if ok { - instructorErrIndex, customerErrCode, _ = txErr.GetCustomErrorCode() - fmt.Printf("now: %s, block: %d, tx: %s, errInstr Code: %d, errInstrIndex: %d, err: %v\n", time.Now().Format("2006-01-02 15:04:05"), ptx.Block, ptx.GetTxHash(), customerErrCode, instructorErrIndex, ptx.Err) - } else { - txs := example.FromTx(ptx) - if len(txs) == 0 { - fmt.Printf("tx is empty, block: %d, tx %s \n", ptx.Block, ptx.GetTxHash()) - continue - } - // printed := false - for _, tx := range txs { - if tx.Program != parser.SolProgramPumpAMM { - continue - } - //if tx.Token1Amount.GreaterThanOrEqual(decimal.NewFromFloat(0.1)) || tx.Event != "buy" { - // continue - //} - // printed = true - fmt.Printf("t: %s, block: %d, is cash:%v hash: %s, maker: %s, program: %s, event: %s, token0: %s, entryContract: %s, token balance: %s, EntryContract: %s\n", - time.Now().Format(time.RFC3339Nano), - tx.Block, tx.Cashback, tx.GetTxHash(), tx.Maker, tx.Program, tx.Event, tx.Token0Amount, tx.EntryContract, tx.AfterSignerToken0Balance, tx.EntryContract) - //break - } - //if !printed { - // continue - //} - //fmt.Printf("t: %s, block: %d, hash: %s, signer: %s, program: %s, event: %s, token0: %s, token1: %s, signer before sol :%s, after sol: %s, after token: %s, tokencreator: %s, tokenprogram: %s, mayhem: %t\n", - // time.Now().Format(time.RFC3339Nano), - // tx.Block, tx.GetTxHash(), tx.Maker, tx.Program, tx.Event, tx.Token0Amount.String(), tx.Token1Amount.String(), - // tx.BeforeSolBalance, tx.AfterSOLBalance, tx.AfterSignerToken0Balance, tx.TokenCreator, tx.Token0Program, tx.Mayhem) - + 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))) } // currentBlock = ptx.Block // diff --git a/internal/example/pump.go b/internal/example/pump.go index 6e2fcd5..dedf840 100644 --- a/internal/example/pump.go +++ b/internal/example/pump.go @@ -3,7 +3,6 @@ package parser import ( "fmt" - "github.com/shopspring/decimal" types "github.com/thloyi/pump-parser" ) @@ -23,24 +22,14 @@ func NewPumpHandler(cb func(*types.Tx)) *PumpHandler { func (h *PumpHandler) HandleMessage(rawTx *types.RawTx) { if rawTx.Meta.Err != nil { // Notify the channel about the failed transaction - beforeSolBalance := decimal.Zero - afterSolBalance := decimal.Zero - if rawTx.Meta.PreBalances != nil && len(rawTx.Meta.PreBalances) > 0 { - beforeSolBalance = decimal.NewFromUint64(rawTx.Meta.PreBalances[0]).Div(decimal.NewFromInt(1e9)) + var parsedTx = &types.Tx{} + parsedTx.SetRawTx(rawTx) + err := parsedTx.Parser() + if err != nil { + fmt.Printf("parser failed tx error: %s, block: %d tx: %s\n", err, rawTx.Slot, rawTx.TxHash()) + return } - if rawTx.Meta.PostBalances != nil && len(rawTx.Meta.PostBalances) > 0 { - afterSolBalance = decimal.NewFromUint64(rawTx.Meta.PostBalances[0]).Div(decimal.NewFromInt(1e9)) - } - h.callback(&types.Tx{ - TxHash: (*[64]byte)((rawTx.Transaction.Signatures[0][:])), - Err: rawTx.Meta.Err, - Signer: rawTx.GetSigner(), - Block: rawTx.Slot, - BlockIndex: uint64(rawTx.IndexWithinBlock), - - BeforeSolBalance: beforeSolBalance, - AfterSOLBalance: afterSolBalance, - }) + h.callback(parsedTx) return } diff --git a/internal/example/yellowstone.go b/internal/example/yellowstone.go index fd2652c..ea4eda7 100644 --- a/internal/example/yellowstone.go +++ b/internal/example/yellowstone.go @@ -50,19 +50,18 @@ type Client struct { func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client { var subscription pb.SubscribeRequest - var failed = false + //var failed = true var vote = false subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions) subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{ - Failed: &failed, - Vote: &vote, + //Failed: &failed, + Vote: &vote, } subscription.Transactions["transactions_sub"].AccountInclude = []string{ "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump } - subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta) subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{} @@ -83,12 +82,12 @@ func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Client { var subscription pb.SubscribeRequest - var failed = false + //var failed = false var vote = false subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions) subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{ - Failed: &failed, - Vote: &vote, + //Failed: &failed, + Vote: &vote, } subscription.Transactions["transactions_sub"].AccountInclude = []string{ diff --git a/internal/test2/test.go b/internal/test2/test.go index bba9a8b..ccdd395 100644 --- a/internal/test2/test.go +++ b/internal/test2/test.go @@ -23,7 +23,7 @@ var () func main() { - var slot uint64 = 399477968 + var slot uint64 = 403021435 var data = NewBlockData(decimal.NewFromFloat(100.0)) client := rpc.New("https://staked.helius-rpc.com?api-key=5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d") var rewards = false @@ -53,9 +53,9 @@ func main() { fmt.Println("from rpc tx error:", i, err) break } - if rawTx.Meta.Err != nil { - continue - } + //if rawTx.Meta.Err != nil { + // continue + //} parsedTx, err := solana_parser.ParseRawTx(rawTx) if err != nil { fmt.Println("parse tx error:", i, rawTx.TxHash(), err) @@ -91,9 +91,7 @@ func main() { fmt.Println("parse action error:", "tx", result.GetTxHash(), "i", i, "err", err) } } - if result.GetTxHash() == "3vMp5Lqm7PxS9MXfXgmptKA9xLkyfKfJpuFs2mUfhRuqTWjGj7DcvSb65NsHQH5RF9JXVLbxrpUHV4LXrgmjXmft" { - fmt.Println("xxx") - } + } fmt.Println("slot", slot, "tx count: ", len(data.Txs)) diff --git a/parser.go b/parser.go index c4bca28..770ba9f 100644 --- a/parser.go +++ b/parser.go @@ -2,7 +2,6 @@ package pump_parser import ( "errors" - "log" "slices" "github.com/gagliardetto/solana-go" @@ -14,6 +13,11 @@ var defaultSwapPrograms = map[solana.PublicKey]swapParser{ pumpProgram: pumpParser, } +var errTxParserPrograms = map[solana.PublicKey]swapParser{ + pumpAmmProgram: pumpAmmParser, + pumpProgram: pumpParser, +} + var swapPrograms = cloneSwapPrograms(defaultSwapPrograms) type ParserOption func(*parserConfig) @@ -97,6 +101,46 @@ func (tx *Tx) Parser() error { tx.Token = make(map[solana.PublicKey]TokenMeta) + if tx.rawTx.Meta.Err != nil { + tx.Err = tx.rawTx.Meta.Err + if tx.Err.UnKnown != "" { + return nil + } + if len(tx.rawTx.Transaction.Message.Instructions) <= int(tx.Err.Index) { + return nil + } + programIdx := tx.rawTx.Transaction.Message.Instructions[tx.Err.Index].ProgramIDIndex + if len(accountList) <= programIdx { + return nil + } + programAccount := accountList[programIdx] + parserFunc, exists := errTxParserPrograms[programAccount] + + if !exists { + return nil + } + // parse failed tx + swaps, _, err := parserFunc(tx, tx.rawTx.Transaction.Message.Instructions[tx.Err.Index], InnerInstructions{}, [2]uint{uint(tx.Err.Index), uint(0)}) + if err != nil { + return nil + //fmt.Printf("parser failed tx error: %s, block: %d tx: %s\n", err, tx.Block, tx.GetTxHash()) + } + if len(swaps) > 0 { + tx.Swaps = swaps + } + for i, instr := range tx.rawTx.Transaction.Message.Instructions { + if p, exists := actionPrograms[programAccount]; exists { + _, err := p(tx, instr, InnerInstructions{}, [2]uint{uint(i), uint(0)}) + if err != nil { + if errors.Is(err, InstructionIgnoredError) { + continue + } + continue + } + } + } + return nil + } var innersMap = make(map[int]InnerInstructions) for _, inner := range tx.rawTx.Meta.InnerInstructions { innersMap[inner.Index] = inner @@ -144,7 +188,7 @@ func (tx *Tx) Parser() error { innerLength := len(innersMap[i].Instructions) for j := 1; j <= innerLength; { if j <= 0 || j > innerLength { - log.Printf("inner instruction index is out if range, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.Block, tx.GetTxHash(), ii, j) + //log.Printf("inner instruction index is out if range, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.Block, tx.GetTxHash(), ii, j) break } innerInstr := innersMap[i].Instructions[j-1] diff --git a/pump.go b/pump.go index dfb0cba..353faf7 100644 --- a/pump.go +++ b/pump.go @@ -34,10 +34,19 @@ func pumpParser(tx *Tx, instruction Instruction, innerInstructions InnerInstruct switch discriminator { case pumpBuyV2Discriminator, pumpBuyDiscriminator, pumpSellDiscriminator: + if tx.Err != nil { + return failedTxBuyOrSellParser(tx, instruction, innerInstructions, offset) + } return BuyOrSellParser(tx, instruction, innerInstructions, offset) case pumpCreateDiscriminator, pumpCreateV2Discriminator: + if tx.Err != nil { + return nil, increaseOffset(offset), InstructionIgnoredError + } return CreateParser(tx, instruction, innerInstructions, offset) case pumpMigrateDiscriminator: + if tx.Err != nil { + return nil, increaseOffset(offset), InstructionIgnoredError + } return MigrateParser(tx, instruction, innerInstructions, offset) default: return nil, increaseOffset(offset), InstructionIgnoredError @@ -178,6 +187,16 @@ type PumpTradeEvent struct { CreatorFeeBasisPoints uint64 CreatorFee uint64 + + TrackVolume bool + TotalUnclaimedTokens uint64 + TotalClaimedTokens uint64 + CurrentSolVolume uint64 + LastUpdateTimestamp int64 + IxName string + MayhemMode bool + CashbackFeeBasisPoints uint64 + Cashback uint64 } type PumpTradeFeeArg struct { @@ -193,6 +212,112 @@ type CompleteEvent struct { Timestamp int64 } +type PumpTradeArgs struct { + Discriminator [8]byte + Amount1 uint64 + Amount2 uint64 +} + +func failedTxBuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + 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]) + } + if tx.Err.Variant != InstructionError { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but error variant is not instruction error, offset, %d, %d", offset[0], offset[1]) + } + if tx.Err.Enum != Custom && tx.Err.Enum != ComputationalBudgetExceeded { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but error is not custom or computational budget exceeded, offset, %d, %d", offset[0], offset[1]) + } + if tx.Err.Enum == Custom { + if !(tx.Err.CustomCode == 1 || + tx.Err.CustomCode == 6042 || + tx.Err.CustomCode == 6041 || + tx.Err.CustomCode == 6040 || + tx.Err.CustomCode == 6023 || tx.Err.CustomCode == 6021 || tx.Err.CustomCode == 6003 || tx.Err.CustomCode == 6002) { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump failed but custom error code is unexpected, offset, %d, %d, code: %d", offset[0], offset[1], tx.Err.CustomCode) + } + } + + result := tx.rawTx + var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + + user := result.accountList[instruction.Accounts[6]] + ataUserIdx := instruction.Accounts[5] + userIndex := instruction.Accounts[6] + mint := result.accountList[instruction.Accounts[2]] + var args PumpTradeArgs + err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump buy/sell decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var event string + var ( + solAmount, tokenAmount uint64 + ) + if bytes.Equal(args.Discriminator[:], pumpBuyV2Discriminator[:]) { + event = "buy_failed" + solAmount = args.Amount1 + tokenAmount = args.Amount2 + } else if bytes.Equal(args.Discriminator[:], pumpBuyDiscriminator[:]) { + event = "buy_failed" + solAmount = args.Amount2 + tokenAmount = args.Amount1 + } else if bytes.Equal(args.Discriminator[:], pumpSellDiscriminator[:]) { + event = "sell_failed" + solAmount = args.Amount2 + tokenAmount = args.Amount1 + } else { + return nil, increaseOffset(offset), fmt.Errorf("unknown pump trade instruction discriminator, offset, %d, %d", offset[0], offset[1]) + } + var baseTokenProgram solana.PublicKey + + if event == "buy_failed" { + baseTokenProgram = result.accountList[instruction.Accounts[8]] + } else { + baseTokenProgram = result.accountList[instruction.Accounts[9]] + } + if !user.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) { + userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, mint) + //&& userBaseAmount.BigInt().Uint64() == tradeEvent.TokenAmount + if !userBaseAmount.IsZero() { + user = result.accountList[0] + userIndex = 0 + ataUserIdx = ataIndex + } + } + + userBase := getAccountBalanceAfterTx(result, ataUserIdx) + userQuote, _ := GetSolAfterTx(result, userIndex) + + bcIdx := instruction.Accounts[3] + bcAtaIndex := instruction.Accounts[4] + solReserves, _ := GetSolAfterTx(result, bcIdx) + tokenReserves := getAccountBalanceAfterTx(result, bcAtaIndex) + swaps := []Swap{ + { + Program: SolProgramPump, + Event: event, + Pool: result.accountList[instruction.Accounts[3]], + BaseMint: mint, + QuoteMint: solana.PublicKey{}, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: solana.PublicKey{}, + BaseMintDecimals: 6, + QuoteMintDecimals: 9, + User: user, + BaseAmount: decimal.NewFromUint64(tokenAmount), + QuoteAmount: decimal.NewFromUint64(solAmount), + BaseReserve: tokenReserves, + QuoteReserve: decimal.NewFromUint64(solReserves), + Mayhem: isMayhemPump(result.accountList[instruction.Accounts[1]]), + UserBaseBalance: userBase, + UserQuoteBalance: decimal.NewFromUint64(userQuote), + EntryContract: entryContract, + }, + } + return swaps, offset, nil +} + func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { result := tx.rawTx var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] @@ -314,6 +439,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns solAmount = solAmount - fee } } + isCashbackCoin := tradeEvent.CashbackFeeBasisPoints > 0 || tradeEvent.Cashback > 0 swaps := []Swap{ { Program: SolProgramPump, @@ -335,6 +461,7 @@ func BuyOrSellParser(tx *Tx, instruction Instruction, innerInstructions InnerIns UserBaseBalance: userBase, UserQuoteBalance: decimal.NewFromUint64(userQuote), EntryContract: entryContract, + Cashback: isCashbackCoin, }, } if completed { diff --git a/pumpamm.go b/pumpamm.go index d163099..ed28214 100644 --- a/pumpamm.go +++ b/pumpamm.go @@ -152,14 +152,29 @@ func pumpAmmParser(tx *Tx, instruction Instruction, innerInstructions InnerInstr discriminator := *(*[8]byte)(decode[:8]) switch discriminator { case pumpAmmCreateDiscriminator: + if tx.Err != nil { + return nil, increaseOffset(offset), InstructionIgnoredError + } return ammCreatePoolParser(tx, instruction, innerInstructions, offset) case pumpAmmBuyDiscriminator, pumpAmmBuyV2Discriminator: + if tx.Err != nil { + return failedTxAmmBuyParser(tx, instruction, innerInstructions, offset) + } return ammBuyParser(tx, instruction, innerInstructions, offset) case pumpAmmSellDiscriminator: + if tx.Err != nil { + return failedTxAmmSellParser(tx, instruction, innerInstructions, offset) + } return ammSellParser(tx, instruction, innerInstructions, offset) case pumpAmmDepositDiscriminator: + if tx.Err != nil { + return nil, increaseOffset(offset), InstructionIgnoredError + } return depositParse(tx, instruction, innerInstructions, offset) case pumpAmmWithdrawDiscriminator: + if tx.Err != nil { + return nil, increaseOffset(offset), InstructionIgnoredError + } return withdrawParse(tx, instruction, innerInstructions, offset) default: return nil, increaseOffset(offset), InstructionIgnoredError @@ -240,6 +255,254 @@ func ammCreatePoolParser(tx *Tx, instruction Instruction, innerInstructions Inne } +type PumpSwapArgs struct { + Discriminator [8]byte + Amount1 uint64 + Amount2 uint64 +} + +func failedTxAmmBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + 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]) + } + if tx.Err.Variant != InstructionError { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error variant is not instruction error, offset, %d, %d", offset[0], offset[1]) + } + if tx.Err.Enum != Custom && tx.Err.Enum != ComputationalBudgetExceeded { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error is not custom or computational budget exceeded, offset, %d, %d", offset[0], offset[1]) + } + if tx.Err.Enum == Custom { + if !(tx.Err.CustomCode == 1 || tx.Err.CustomCode == 6004 || + tx.Err.CustomCode == 6040 || + tx.Err.CustomCode == 6039 || + tx.Err.CustomCode == 6016 || + tx.Err.CustomCode == 6014) { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but custom error code is unexpected, offset, %d, %d, code: %d", offset[0], offset[1], tx.Err.CustomCode) + } + } + + result := tx.rawTx + var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + var args PumpSwapArgs + err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm buy failed decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var event string + var ( + quoteAmount, tokenAmount uint64 + ) + if bytes.Equal(args.Discriminator[:], pumpAmmBuyV2Discriminator[:]) { + event = "buy_failed" + quoteAmount = args.Amount1 + tokenAmount = args.Amount2 + } else if bytes.Equal(args.Discriminator[:], pumpAmmBuyDiscriminator[:]) { + event = "buy_failed" + quoteAmount = args.Amount2 + tokenAmount = args.Amount1 + } else { + return nil, increaseOffset(offset), fmt.Errorf("unknown pump amm trade instruction discriminator, offset, %d, %d", offset[0], offset[1]) + } + + baseMint := result.accountList[instruction.Accounts[3]] + quoteMint := result.accountList[instruction.Accounts[4]] + baseTokenProgram := result.accountList[instruction.Accounts[11]] + quoteTokenProgram := result.accountList[instruction.Accounts[12]] + + poolBaseAccountIdx := instruction.Accounts[7] + poolQuoteAccountIdx := instruction.Accounts[8] + var ( + baseMintDecimals uint8 + quoteMintDecimals uint8 + ) + for _, meta := range result.Meta.PostTokenBalances { + if meta.AccountIndex == poolBaseAccountIdx { + baseMintDecimals = uint8(meta.UITokenAmount.Decimals) + } else if meta.AccountIndex == poolQuoteAccountIdx { + quoteMintDecimals = uint8(meta.UITokenAmount.Decimals) + } + } + if _, exists := tx.Token[baseMint]; !exists && !baseMint.Equals(wSolMint) { + tx.Token[baseMint] = TokenMeta{ + Mint: baseMint, + Decimals: baseMintDecimals, + TokenProgram: baseTokenProgram, + } + } + + if _, exists := tx.Token[quoteMint]; !exists && !quoteMint.Equals(wSolMint) { + tx.Token[quoteMint] = TokenMeta{ + Mint: quoteMint, + Decimals: quoteMintDecimals, + TokenProgram: quoteTokenProgram, + } + } + + var eventUser = tx.rawTx.accountList[instruction.Accounts[1]] + + baseMintAtaUserIdx := instruction.Accounts[5] + userIndex := instruction.Accounts[1] + if !eventUser.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) { + userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, baseMint) + // && userBaseAmount.BigInt().Uint64() == event.BaseAmountOut + if !userBaseAmount.IsZero() { + eventUser = result.accountList[0] + userIndex = 0 + baseMintAtaUserIdx = ataIndex + } + } + + userBase := getAccountBalanceAfterTx(result, baseMintAtaUserIdx) + userQuote := GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint) + + if quoteMint.Equals(wSolMint) { + userBalance, _ := GetSolAfterTx(result, userIndex) + userQuote = userQuote.Add(decimal.NewFromUint64(userBalance)) + } + baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7]) + quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8]) + return []Swap{ + { + Program: SolProgramPumpAMM, + Event: event, + Pool: tx.rawTx.accountList[instruction.Accounts[0]], + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + User: eventUser, + BaseAmount: decimal.NewFromUint64(tokenAmount), + QuoteAmount: decimal.NewFromUint64(quoteAmount), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + EntryContract: entryContract, + }, + }, offset, nil +} + +func failedTxAmmSellParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { + 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]) + } + if tx.Err.Variant != InstructionError { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error variant is not instruction error, offset, %d, %d", offset[0], offset[1]) + } + if tx.Err.Enum != Custom && tx.Err.Enum != ComputationalBudgetExceeded { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but error is not custom or computational budget exceeded, offset, %d, %d", offset[0], offset[1]) + } + if tx.Err.Enum == Custom { + if !(tx.Err.CustomCode == 1 || tx.Err.CustomCode == 6004 || + tx.Err.CustomCode == 6040 || + tx.Err.CustomCode == 6039 || + tx.Err.CustomCode == 6016 || + tx.Err.CustomCode == 6014) { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm sell failed but custom error code is unexpected, offset, %d, %d, code: %d", offset[0], offset[1], tx.Err.CustomCode) + } + } + result := tx.rawTx + var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] + var args PumpSwapArgs + err := agbinary.NewBorshDecoder(instruction.Data[:]).Decode(&args) + if err != nil { + return nil, increaseOffset(offset), fmt.Errorf("failed tx pump amm buy failed decode error: %v, offset, %d, %d", err, offset[0], offset[1]) + } + var event string + var ( + quoteAmount, tokenAmount uint64 + ) + if bytes.Equal(args.Discriminator[:], pumpAmmSellDiscriminator[:]) { + event = "sell_failed" + tokenAmount = args.Amount1 + quoteAmount = args.Amount2 + } else { + return nil, increaseOffset(offset), fmt.Errorf("unknown pump amm trade instruction discriminator, offset, %d, %d", offset[0], offset[1]) + } + baseMint := result.accountList[instruction.Accounts[3]] + quoteMint := result.accountList[instruction.Accounts[4]] + baseTokenProgram := result.accountList[instruction.Accounts[11]] + quoteTokenProgram := result.accountList[instruction.Accounts[12]] + + poolBaseAccountIdx := instruction.Accounts[7] + poolQuoteAccountIdx := instruction.Accounts[8] + var ( + baseMintDecimals uint8 + quoteMintDecimals uint8 + ) + for _, meta := range result.Meta.PostTokenBalances { + if meta.AccountIndex == poolBaseAccountIdx { + baseMintDecimals = uint8(meta.UITokenAmount.Decimals) + } else if meta.AccountIndex == poolQuoteAccountIdx { + quoteMintDecimals = uint8(meta.UITokenAmount.Decimals) + } + } + if _, exists := tx.Token[baseMint]; !exists && !baseMint.Equals(wSolMint) { + tx.Token[baseMint] = TokenMeta{ + Mint: baseMint, + Decimals: baseMintDecimals, + TokenProgram: baseTokenProgram, + } + } + + if _, exists := tx.Token[quoteMint]; !exists && !quoteMint.Equals(wSolMint) { + tx.Token[quoteMint] = TokenMeta{ + Mint: quoteMint, + Decimals: quoteMintDecimals, + TokenProgram: quoteTokenProgram, + } + } + + var eventUser = tx.rawTx.accountList[instruction.Accounts[1]] + + baseMintAtaUserIdx := instruction.Accounts[5] + userIndex := instruction.Accounts[1] + if !eventUser.IsOnCurve() && (entryContract.Equals(okxDexRoutersV2) || entryContract.Equals(okxAggregatorV2)) { + userBaseAmount, ataIndex := tokenBalanceChange(result, 0, baseTokenProgram, baseMint) + // && userBaseAmount.BigInt().Uint64() == event.BaseAmountIn + if !userBaseAmount.IsZero() { + eventUser = result.accountList[0] + userIndex = 0 + baseMintAtaUserIdx = ataIndex + } + } + + userBase := getAccountBalanceAfterTx(result, baseMintAtaUserIdx) + userQuote := GetTokenBalanceAfterTx(result, userIndex, quoteTokenProgram, quoteMint) + + if quoteMint.Equals(wSolMint) { + userBalance, _ := GetSolAfterTx(result, userIndex) + userQuote = userQuote.Add(decimal.NewFromUint64(userBalance)) + } + baseReserve := getAccountBalanceAfterTx(result, instruction.Accounts[7]) + quoteReserve := getAccountBalanceAfterTx(result, instruction.Accounts[8]) + return []Swap{ + { + Program: SolProgramPumpAMM, + Event: event, + Pool: tx.rawTx.accountList[instruction.Accounts[0]], + BaseMint: baseMint, + QuoteMint: quoteMint, + BaseTokenProgram: baseTokenProgram, + QuoteTokenProgram: quoteTokenProgram, + BaseMintDecimals: baseMintDecimals, + QuoteMintDecimals: quoteMintDecimals, + User: eventUser, + BaseAmount: decimal.NewFromUint64(tokenAmount), + QuoteAmount: decimal.NewFromUint64(quoteAmount), + BaseReserve: baseReserve, + QuoteReserve: quoteReserve, + Mayhem: isMayhemPump(result.accountList[instruction.Accounts[9]]), + UserBaseBalance: userBase, + UserQuoteBalance: userQuote, + EntryContract: entryContract, + }, + }, offset, nil +} + func ammBuyParser(tx *Tx, instruction Instruction, innerInstructions InnerInstructions, offset [2]uint) ([]Swap, [2]uint, error) { result := tx.rawTx var entryContract = result.accountList[result.Transaction.Message.Instructions[offset[0]].ProgramIDIndex] diff --git a/rawtx.go b/rawtx.go index 49aac83..00bf158 100644 --- a/rawtx.go +++ b/rawtx.go @@ -73,6 +73,10 @@ func (tx *RawTx) GetAccountLust() []solana.PublicKey { return tx.getAccountList() } +func (tx *RawTx) GetAccountList() []solana.PublicKey { + return tx.getAccountList() +} + func (tx *RawTx) TxHash() string { if len(tx.Transaction.Signatures) > 0 { return tx.Transaction.Signatures[0].String() @@ -146,17 +150,17 @@ func (tb *TokenBalance) ParseAccount() { } type Meta struct { - Err interface{} `json:"err"` - Fee uint64 `json:"fee"` - InnerInstructions []InnerInstructions `json:"innerInstructions"` - LoadedAddresses LoadedAddresses `json:"loadedAddresses"` - LogMessages []string `json:"logMessages"` - PostBalances []uint64 `json:"postBalances"` - PostTokenBalances []TokenBalance `json:"postTokenBalances"` - PreBalances []uint64 `json:"preBalances"` - PreTokenBalances []TokenBalance `json:"preTokenBalances"` - Rewards []interface{} `json:"rewards"` - ComputeUnitsConsumed uint64 `json:"computeUnitsConsumed"` + Err *TransactionParsedError `json:"err"` + Fee uint64 `json:"fee"` + InnerInstructions []InnerInstructions `json:"innerInstructions"` + LoadedAddresses LoadedAddresses `json:"loadedAddresses"` + LogMessages []string `json:"logMessages"` + PostBalances []uint64 `json:"postBalances"` + PostTokenBalances []TokenBalance `json:"postTokenBalances"` + PreBalances []uint64 `json:"preBalances"` + PreTokenBalances []TokenBalance `json:"preTokenBalances"` + Rewards []interface{} `json:"rewards"` + ComputeUnitsConsumed uint64 `json:"computeUnitsConsumed"` } type Header struct { NumReadonlySignedAccounts int `json:"numReadonlySignedAccounts"` @@ -294,6 +298,16 @@ func InstructionsFromRpc(instructions []solana.CompiledInstruction) []Instructio return instrs } +type RpcTransactionErr []interface{} + +func marshalRpcTransactionErr(err any) string { + e, _ := json.Marshal(err) + if len(e) == 0 { + return "UnKnown" + } + return string(e) +} + func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, slot uint64, index int64) (*RawTx, error) { created := int64(0) if blockTime != nil { @@ -321,8 +335,62 @@ func FromRpcTransactionWithMeta(tx rpc.TransactionWithMeta, blockTime *uint64, s yTx, _ := tx.GetTransaction() if meta.Err != nil { - e, _ := json.Marshal(meta.Err) - sTx.Meta.Err = string(e) + if iErr, ok := meta.Err.(map[string]any); ok { + instructionError := iErr["InstructionError"] + if instructionError == nil { + sTx.Meta.Err = &TransactionParsedError{ + UnKnown: marshalRpcTransactionErr(meta.Err), + } + } else { + if oErr, ok := instructionError.([]any); ok { + if len(oErr) <= 1 { + sTx.Meta.Err = &TransactionParsedError{ + UnKnown: marshalRpcTransactionErr(meta.Err), + } + } else if instrIdx, ok := oErr[0].(float64); ok { + sTx.Meta.Err = &TransactionParsedError{ + Index: uint8(instrIdx), + Variant: InstructionError, + } + errDetail, ok := oErr[1].(string) + if ok { + if errDetail == "ComputationalBudgetExceeded" { + sTx.Meta.Err.Enum = ComputationalBudgetExceeded + } else { + sTx.Meta.Err.UnKnown = errDetail + } + } else { + errDetail2, ok := oErr[1].(map[string]any) + if ok && len(errDetail2) > 0 && errDetail2["Custom"] != nil { + custom, ok := errDetail2["Custom"].(float64) + if ok { + sTx.Meta.Err.Enum = Custom + sTx.Meta.Err.CustomCode = uint32(custom) + } else { + sTx.Meta.Err.UnKnown = marshalRpcTransactionErr(meta.Err) + } + + } else { + sTx.Meta.Err.UnKnown = marshalRpcTransactionErr(meta.Err) + } + } + } else { + sTx.Meta.Err = &TransactionParsedError{ + UnKnown: marshalRpcTransactionErr(meta.Err), + } + } + } else { + sTx.Meta.Err = &TransactionParsedError{ + UnKnown: marshalRpcTransactionErr(meta.Err), + } + } + } + } else { + sTx.Meta.Err = &TransactionParsedError{ + UnKnown: marshalRpcTransactionErr(meta.Err), + } + } + } sTx.Meta.Fee = meta.Fee //sTx.Meta.InnerInstructions = meta.InnerInstructions @@ -799,12 +867,7 @@ func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateT if meta.Err != nil && len(meta.Err.GetErr()) > 0 { // If the transaction has an error, we set the error in the Meta - transError, err := DecodeTransactionError(meta.Err.GetErr()) - if err != nil { - sTx.Meta.Err = err - } else { - sTx.Meta.Err = transError - } + sTx.Meta.Err = ParseTransactionErrorFromGeyser(meta.Err.GetErr()) // sTx.Meta.Err = meta.Err.GetErr() } sTx.Meta.Fee = meta.Fee diff --git a/tx.go b/tx.go index ca5d8e9..379b89f 100644 --- a/tx.go +++ b/tx.go @@ -79,13 +79,13 @@ type Tx struct { rawTx *RawTx Vote bool Signer solana.PublicKey - Err interface{} `json:"err,omitempty"` - Swaps []Swap `json:"swaps,omitempty"` - SolTransfer []SolTransfer `json:"sol_transfer,omitempty"` - Block uint64 `json:"block"` - BlockIndex uint64 `json:"index"` - TxHash *[64]byte `json:"-"` - BlockAt int64 `json:"block_at"` + Err *TransactionParsedError `json:"err,omitempty"` + Swaps []Swap `json:"swaps,omitempty"` + SolTransfer []SolTransfer `json:"sol_transfer,omitempty"` + Block uint64 `json:"block"` + BlockIndex uint64 `json:"index"` + TxHash *[64]byte `json:"-"` + BlockAt int64 `json:"block_at"` CuFee decimal.Decimal `json:"cu_fee"`