package pump_parser import ( "encoding/json" "fmt" "time" bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "github.com/jackc/pgtype" "github.com/shopspring/decimal" ) func (tx *RawTx) getAccountList() []solana.PublicKey { if tx.accountList != nil { return tx.accountList } length := len(tx.Transaction.Message.AccountKeys) + len(tx.Meta.LoadedAddresses.Writable) + len(tx.Meta.LoadedAddresses.Readonly) tx.accountList = make([]solana.PublicKey, length) var i = 0 for _, v := range tx.Transaction.Message.AccountKeys { tx.accountList[i] = v i++ } for _, v := range tx.Meta.LoadedAddresses.Writable { tx.accountList[i] = v i++ } for _, v := range tx.Meta.LoadedAddresses.Readonly { tx.accountList[i] = v i++ } return tx.accountList } func (tx *RawTx) GetSigner() solana.PublicKey { accountList := tx.getAccountList() if len(accountList) > 0 { return accountList[0] } return solana.PublicKey{} } type RPCResponse struct { JsonRPC string `json:"jsonrpc"` Result RawTx `json:"result"` ID int `json:"id"` } type RawTx struct { accountList []solana.PublicKey BlockTime int64 `json:"blockTime"` IndexWithinBlock int64 `json:"indexWithinBlock"` Meta Meta `json:"meta"` Slot uint64 `json:"slot"` Transaction Transaction `json:"transaction"` Version interface{} `json:"version"` //Platform string `json:"platform,omitempty"` //PlatformFee decimal.Decimal `json:"-"` //CUPrice decimal.Decimal `json:"CUPrice,omitempty"` //MevAgent string `json:"mevAgent,omitempty"` //MevAgentFee decimal.Decimal `json:"mevAgentFee,omitempty"` //EntryContract []string `json:"entryContract,omitempty"` } func (tx *RawTx) GetAccountLust() []solana.PublicKey { return tx.getAccountList() } func (tx *RawTx) TxHash() string { if len(tx.Transaction.Signatures) > 0 { return tx.Transaction.Signatures[0].String() } return "" } func (tx *RawTx) GetSignerAfterBalance() decimal.Decimal { if len(tx.Meta.PostBalances) > 0 { return decimal.New(int64(tx.Meta.PostBalances[0]), -9) } return decimal.Zero } func (tx *RawTx) GetSignerBeforeBalance() decimal.Decimal { if len(tx.Meta.PreBalances) > 0 { return decimal.New(int64(tx.Meta.PreBalances[0]), -9) } return decimal.Zero } func (tx *RawTx) GetBlockTime() *pgtype.Timestamptz { t := pgtype.Timestamptz{} t.Set(time.Unix(tx.BlockTime, 0)) return &t } type Instruction struct { Accounts []int `json:"accounts"` Data solana.Base58 `json:"data"` ProgramIDIndex int `json:"programIdIndex"` StackHeight *int `json:"stackHeight"` } type InnerInstructions struct { Index int `json:"index"` Instructions []Instruction `json:"instructions"` } type LoadedAddresses struct { Readonly solana.PublicKeySlice `json:"readonly"` Writable solana.PublicKeySlice `json:"writable"` } type UITokenAmount struct { Amount string `json:"amount"` Decimals uint64 `json:"decimals"` UIAmount float64 `json:"uiAmount"` UIAmountString string `json:"uiAmountString"` } type TokenBalance struct { AccountIndex int `json:"accountIndex"` MintAccount solana.PublicKey `json:"mint_account"` OwnerAccount *solana.PublicKey `json:"owner_account"` ProgramIDAccount solana.PublicKey `json:"programId_account"` Mint string `json:"mint"` Owner string `json:"owner"` ProgramID string `json:"programId"` UITokenAmount UITokenAmount `json:"uiTokenAmount"` } func (tb *TokenBalance) ParseAccount() { if tb.Mint == "" { tb.Mint = tb.MintAccount.String() } if tb.Owner == "" && tb.OwnerAccount != nil { tb.Owner = tb.OwnerAccount.String() } if tb.ProgramID == "" { tb.ProgramID = tb.ProgramIDAccount.String() } } 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"` } type Header struct { NumReadonlySignedAccounts int `json:"numReadonlySignedAccounts"` NumReadonlyUnsignedAccounts int `json:"numReadonlyUnsignedAccounts"` NumRequiredSignatures int `json:"numRequiredSignatures"` } type Message struct { AccountKeys solana.PublicKeySlice `json:"accountKeys"` AddressTableLookups solana.MessageAddressTableLookupSlice `json:"addressTableLookups"` Header Header `json:"header"` Instructions []Instruction `json:"instructions"` RecentBlockHash string `json:"recentBlockhash"` } type Transaction struct { Message Message `json:"message"` Signatures []solana.Signature `json:"signatures"` } func (tx *Transaction) UnmarshalJSON(data []byte) error { if len(data) == 0 || (len(data) == 4 && string(data) == "null") { // TODO: is this an error? return nil } firstChar := data[0] switch firstChar { // Check if first character is `[`, standing for a JSON array. case '[': // It's base64 (or similar) { var asDecodedBinary solana.Data err := asDecodedBinary.UnmarshalJSON(data) if err != nil { return err } asParsedTransaction := new(solana.Transaction) err = asParsedTransaction.UnmarshalWithDecoder(bin.NewBinDecoder(asDecodedBinary.Content)) if err != nil { return err } tx.Message = Message{ AccountKeys: asParsedTransaction.Message.AccountKeys, AddressTableLookups: asParsedTransaction.Message.AddressTableLookups, Header: Header{ NumReadonlySignedAccounts: int(asParsedTransaction.Message.Header.NumReadonlySignedAccounts), NumReadonlyUnsignedAccounts: int(asParsedTransaction.Message.Header.NumReadonlyUnsignedAccounts), NumRequiredSignatures: int(asParsedTransaction.Message.Header.NumRequiredSignatures), }, Instructions: InstructionsFromRpc(asParsedTransaction.Message.Instructions), RecentBlockHash: asParsedTransaction.Message.RecentBlockhash.String(), } tx.Signatures = asParsedTransaction.Signatures } case '{': // It's JSON, most likely. { var asParsedTransaction solana.Transaction err := json.Unmarshal(data, &asParsedTransaction) if err != nil { return err } tx.Message = Message{ AccountKeys: asParsedTransaction.Message.AccountKeys, AddressTableLookups: asParsedTransaction.Message.AddressTableLookups, Header: Header{ NumReadonlySignedAccounts: int(asParsedTransaction.Message.Header.NumReadonlySignedAccounts), NumReadonlyUnsignedAccounts: int(asParsedTransaction.Message.Header.NumReadonlyUnsignedAccounts), NumRequiredSignatures: int(asParsedTransaction.Message.Header.NumRequiredSignatures), }, Instructions: InstructionsFromRpc(asParsedTransaction.Message.Instructions), RecentBlockHash: asParsedTransaction.Message.RecentBlockhash.String(), } tx.Signatures = asParsedTransaction.Signatures return nil } default: return fmt.Errorf("Unknown kind: %v", data) } return nil } type ParsedTx struct { AccountData []struct { Account string `json:"account"` NativeBalanceChange float64 `json:"nativeBalanceChange"` TokenBalanceChanges []interface{} `json:"tokenBalanceChanges"` } `json:"accountData"` Description string `json:"description"` Fee int `json:"fee"` FeePayer string `json:"feePayer"` Instructions []struct { Accounts []string `json:"accounts"` Data string `json:"data"` InnerInstructions []struct { Accounts []string `json:"accounts"` Data string `json:"data"` ProgramID string `json:"programId"` } `json:"innerInstructions"` ProgramID string `json:"programId"` } `json:"instructions"` NativeTransfers []struct { Amount float64 `json:"amount"` FromUserAccount string `json:"fromUserAccount"` ToUserAccount string `json:"toUserAccount"` } `json:"nativeTransfers"` Signature string `json:"signature"` Slot int `json:"slot"` Source string `json:"source"` Timestamp int `json:"timestamp"` TokenTransfers []struct { FromTokenAccount string `json:"fromTokenAccount"` FromUserAccount string `json:"fromUserAccount"` Mint string `json:"mint"` ToTokenAccount string `json:"toTokenAccount"` ToUserAccount string `json:"toUserAccount"` TokenAmount float64 `json:"tokenAmount"` TokenStandard string `json:"tokenStandard"` } `json:"tokenTransfers"` TransactionError interface{} `json:"transactionError"` Type string `json:"type"` } func InstructionsFromRpc(instructions []solana.CompiledInstruction) []Instruction { var instrs []Instruction = make([]Instruction, len(instructions)) for i, instruction := range instructions { instrs[i] = Instruction{ Accounts: intSliceFromUint16Slice(instruction.Accounts), Data: instruction.Data, ProgramIDIndex: int(instruction.ProgramIDIndex), } } return instrs } func InnerInstructionsFromRpc(instructions []rpc.InnerInstruction) []InnerInstructions { var innerInstructions []InnerInstructions = make([]InnerInstructions, len(instructions)) for i, instruction := range instructions { //instruction.Instructions instrs := make([]Instruction, len(instruction.Instructions)) for j, instr := range instruction.Instructions { instrs[j] = Instruction{ Accounts: intSliceFromUint16Slice(instr.Accounts), Data: instr.Data, ProgramIDIndex: int(instr.ProgramIDIndex), //StackHeight: instr.StackHeight, } } innerInstructions[i] = InnerInstructions{ Index: int(instruction.Index), Instructions: instrs, } } return innerInstructions } func intSliceFromUint16Slice(in []uint16) []int { out := make([]int, len(in)) for i, v := range in { out[i] = int(v) } return out } func getTokenBalanceAfterTx(result *RawTx, accountIndex int) (*TokenBalance, error) { var preBalance *TokenBalance for _, pre := range result.Meta.PreTokenBalances { if pre.AccountIndex == accountIndex { preBalance = &pre break } } var postBalance *TokenBalance for _, post := range result.Meta.PostTokenBalances { if post.AccountIndex == accountIndex { post.ParseAccount() postBalance = &post break } } if preBalance == nil && postBalance == nil { return nil, fmt.Errorf("account not found") } if postBalance == nil { preBalance.ParseAccount() return &TokenBalance{ AccountIndex: preBalance.AccountIndex, MintAccount: preBalance.MintAccount, OwnerAccount: preBalance.OwnerAccount, ProgramIDAccount: preBalance.ProgramIDAccount, Mint: preBalance.Mint, Owner: preBalance.Owner, ProgramID: preBalance.ProgramID, UITokenAmount: UITokenAmount{ Amount: "0", Decimals: preBalance.UITokenAmount.Decimals, UIAmount: 0, UIAmountString: "0", }, }, nil } return postBalance, nil } func getAccountBalanceAfterTx(result *RawTx, accountIndex int) decimal.Decimal { x, err := getTokenBalanceAfterTx(result, accountIndex) if err != nil { return decimal.Zero } amount, err := decimal.NewFromString(x.UITokenAmount.Amount) if err != nil { return decimal.Zero } return amount } func GetTokenBalanceAfterTx(result *RawTx, accountIndex int, tokenProgram, mint solana.PublicKey) decimal.Decimal { ataAccount, _, _ := solana.FindProgramAddress([][]byte{ result.accountList[accountIndex][:], tokenProgram[:], mint[:], }, solana.SPLAssociatedTokenAccountProgramID) ataIndex := 0 for i, account := range result.accountList { if account.Equals(ataAccount) { ataIndex = i break } } if ataIndex == 0 { return decimal.Zero } x, err := getTokenBalanceAfterTx(result, ataIndex) if err != nil { return decimal.Zero } amount, err := decimal.NewFromString(x.UITokenAmount.Amount) if err != nil { return decimal.Zero } return amount } func GetSolAfterTx(result *RawTx, accountIndex int) (uint64, error) { for i, post := range result.Meta.PostBalances { if i == accountIndex { return post, nil } } return 0, fmt.Errorf("account not found") } func solSplAccount(owner, mint solana.PublicKey) (solana.PublicKey, error) { ataAddress, _, err := solana.FindAssociatedTokenAddress(owner, mint) if err != nil { return solana.PublicKey{}, err } return ataAddress, nil } func solSpl2022Account(owner, mint solana.PublicKey) (solana.PublicKey, error) { address, _, err := solana.FindProgramAddress([][]byte{ owner[:], solana.Token2022ProgramID[:], mint[:], }, solana.SPLAssociatedTokenAccountProgramID, ) return address, err } func isAccountOwner(account, owner, mint solana.PublicKey) (bool, error) { ata, err := solSplAccount(owner, mint) if err != nil { return false, err } if account == ata { return true, nil } ata, err = solSpl2022Account(owner, mint) if err != nil { return false, err } return account == ata, nil }