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" pb "go.onsig.ai/onsig/yellowstone-proto" ) 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) GetAccountList() []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 *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"` 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 } 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 { created = int64(*blockTime) } sTx := &RawTx{ BlockTime: created, Slot: slot, IndexWithinBlock: index, Meta: Meta{ Err: nil, Fee: 0, InnerInstructions: nil, LoadedAddresses: LoadedAddresses{}, LogMessages: nil, PostBalances: nil, PostTokenBalances: nil, PreBalances: nil, PreTokenBalances: nil, Rewards: nil, }, } meta := tx.Meta yTx, _ := tx.GetTransaction() if meta.Err != nil { 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" || errDetail == "ProgramFailedToComplete" { 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 if meta.ComputeUnitsConsumed != nil { sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed } for _, innerInstr := range meta.InnerInstructions { var instrs []Instruction for _, instr := range innerInstr.Instructions { instrs = append(instrs, Instruction{ ProgramIDIndex: int(instr.ProgramIDIndex), Accounts: func() []int { var out []int for i := range instr.Accounts { out = append(out, int(instr.Accounts[i])) } return out }(), Data: instr.Data, StackHeight: newInt16(instr.StackHeight), }) } sTx.Meta.InnerInstructions = append(sTx.Meta.InnerInstructions, InnerInstructions{ Index: int(innerInstr.Index), Instructions: instrs, }) } sTx.Meta.LogMessages = meta.LogMessages sTx.Meta.PostBalances = meta.PostBalances sTx.Meta.PreBalances = meta.PreBalances sTx.Meta.PostTokenBalances = convertTokenBalanceFromRpc(meta.PostTokenBalances) sTx.Meta.PreTokenBalances = convertTokenBalanceFromRpc(meta.PreTokenBalances) sTx.Meta.Rewards = nil sTx.Meta.LoadedAddresses.Readonly = meta.LoadedAddresses.ReadOnly sTx.Meta.LoadedAddresses.Writable = meta.LoadedAddresses.Writable // copy signatures for i := range yTx.Signatures { sTx.Transaction.Signatures = append(sTx.Transaction.Signatures, yTx.Signatures[i]) } // copy message sTx.Transaction.Message = Message{ RecentBlockHash: yTx.Message.RecentBlockhash.String(), } // copy message.AccountKeys //stopAt := len(yTx.Message.AccountKeys) - sTx.Message.NumLookups() stopAt := len(yTx.Message.AccountKeys) for accIndex, acc := range yTx.Message.AccountKeys { sTx.Transaction.Message.AccountKeys = append(sTx.Transaction.Message.AccountKeys, acc) if accIndex == stopAt-1 { break } } // copy message.Header sTx.Transaction.Message.Header = Header{ NumRequiredSignatures: int(yTx.Message.Header.NumRequiredSignatures), NumReadonlySignedAccounts: int(yTx.Message.Header.NumReadonlySignedAccounts), NumReadonlyUnsignedAccounts: int(yTx.Message.Header.NumReadonlyUnsignedAccounts), } // copy message.versioned if yTx.Message.IsVersioned() { sTx.Version = solana.MessageVersionV0 } else { sTx.Version = solana.MessageVersionLegacy } // copy address table lookups { tables := map[solana.PublicKey]solana.PublicKeySlice{} writable := meta.LoadedAddresses.Writable readonly := meta.LoadedAddresses.ReadOnly for _, addr := range yTx.Message.AddressTableLookups { sTx.Transaction.Message.AddressTableLookups = append(sTx.Transaction.Message.AddressTableLookups, solana.MessageAddressTableLookup{ AccountKey: addr.AccountKey, WritableIndexes: addr.WritableIndexes, ReadonlyIndexes: addr.ReadonlyIndexes, }) numTakeWritable := len(addr.WritableIndexes) numTakeReadonly := len(addr.ReadonlyIndexes) tableKey := addr.AccountKey { // now need to rebuild the address table taking into account the indexes, and put the keys into the tables maxIndex := 0 for _, indexB := range addr.WritableIndexes { index := int(indexB) if index > maxIndex { maxIndex = index } } for _, indexB := range addr.ReadonlyIndexes { index := int(indexB) if index > maxIndex { maxIndex = index } } tables[tableKey] = make([]solana.PublicKey, maxIndex+1) } if numTakeWritable > 0 { writableForTable := writable[:numTakeWritable] for i, indexB := range addr.WritableIndexes { index := int(indexB) tables[tableKey][index] = writableForTable[i] } writable = writable[numTakeWritable:] } if numTakeReadonly > 0 { readableForTable := readonly[:numTakeReadonly] for i, indexB := range addr.ReadonlyIndexes { index := int(indexB) tables[tableKey][index] = readableForTable[i] } readonly = readonly[numTakeReadonly:] } } } // copy instructions for _, instr := range yTx.Message.Instructions { sTx.Transaction.Message.Instructions = append(sTx.Transaction.Message.Instructions, Instruction{ ProgramIDIndex: int(instr.ProgramIDIndex), Accounts: func() []int { var out []int for i := range instr.Accounts { out = append(out, int(instr.Accounts[i])) } return out }(), Data: instr.Data, }) } return sTx, nil } func convertTokenBalanceFromRpc(tb []rpc.TokenBalance) []TokenBalance { var tokenBalances []TokenBalance = make([]TokenBalance, len(tb)) for i, balance := range tb { var uiAmount = float64(0) if balance.UiTokenAmount.UiAmount != nil { uiAmount = *balance.UiTokenAmount.UiAmount } tokenBalances[i] = TokenBalance{ AccountIndex: int(balance.AccountIndex), MintAccount: balance.Mint, OwnerAccount: balance.Owner, ProgramIDAccount: func() solana.PublicKey { if balance.ProgramId != nil { return *balance.ProgramId } return solana.PublicKey{} }(), UITokenAmount: UITokenAmount{ Amount: balance.UiTokenAmount.Amount, Decimals: uint64(balance.UiTokenAmount.Decimals), UIAmount: uiAmount, UIAmountString: balance.UiTokenAmount.UiAmountString, }, } } return tokenBalances } 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 getAtaIdxByOwner(result *RawTx, owner solana.PublicKey, mint solana.PublicKey) (int, error) { var preBalance *TokenBalance for _, pre := range result.Meta.PreTokenBalances { if pre.MintAccount == mint && pre.OwnerAccount != nil && pre.OwnerAccount.Equals(owner) { preBalance = &pre break } } var postBalance *TokenBalance for _, post := range result.Meta.PostTokenBalances { if post.MintAccount == mint && post.OwnerAccount != nil && post.OwnerAccount.Equals(owner) { // post.ParseAccount() postBalance = &post break } } if preBalance == nil && postBalance == nil { return 0, fmt.Errorf("account not found") } if preBalance != nil && postBalance != nil && preBalance.AccountIndex != postBalance.AccountIndex { return 0, fmt.Errorf("ata index not match") } if postBalance == nil { return preBalance.AccountIndex, nil } return postBalance.AccountIndex, nil } func getAtaByOwner(result *RawTx, owner solana.PublicKey, mint solana.PublicKey) (*TokenBalance, error) { var preBalance *TokenBalance for _, pre := range result.Meta.PreTokenBalances { if pre.MintAccount == mint && pre.OwnerAccount != nil && pre.OwnerAccount.Equals(owner) { preBalance = &pre break } } var postBalance *TokenBalance for _, post := range result.Meta.PostTokenBalances { if post.MintAccount == mint && post.OwnerAccount != nil && post.OwnerAccount.Equals(owner) { // post.ParseAccount() postBalance = &post break } } if preBalance == nil && postBalance == nil { return nil, fmt.Errorf("account not found") } if preBalance != nil && postBalance != nil && preBalance.AccountIndex != postBalance.AccountIndex { return nil, fmt.Errorf("ata index not match") } 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 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 tokenBalanceChange(result *RawTx, accountIndex int, tokenProgram, mint solana.PublicKey) (change decimal.Decimal, ataIndex int) { ataAccount, _, _ := solana.FindProgramAddress([][]byte{ result.accountList[accountIndex][:], tokenProgram[:], mint[:], }, solana.SPLAssociatedTokenAccountProgramID) for i, account := range result.accountList { if account.Equals(ataAccount) { ataIndex = i break } } var err error if ataIndex == 0 { ataIndex, err = getAtaIdxByOwner(result, result.accountList[accountIndex], mint) if err != nil { return decimal.Zero, ataIndex } } before := decimal.Zero after := decimal.Zero for _, pre := range result.Meta.PreTokenBalances { if pre.AccountIndex == ataIndex { amount, err := decimal.NewFromString(pre.UITokenAmount.Amount) if err != nil { return decimal.Zero, ataIndex } before = amount break } } for _, post := range result.Meta.PostTokenBalances { if post.AccountIndex == ataIndex { amount, err := decimal.NewFromString(post.UITokenAmount.Amount) if err != nil { return decimal.Zero, ataIndex } after = amount break } } return after.Sub(before).Abs(), ataIndex } 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 } } var x *TokenBalance var err error if ataIndex == 0 { x, err = getAtaByOwner(result, result.accountList[accountIndex], mint) } else { 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 } func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64) (*RawTx, error) { sTx := &RawTx{ BlockTime: created, Slot: y.Slot, IndexWithinBlock: int64(y.Transaction.Index), Meta: Meta{ Err: nil, Fee: 0, InnerInstructions: nil, LoadedAddresses: LoadedAddresses{}, LogMessages: nil, PostBalances: nil, PostTokenBalances: nil, PreBalances: nil, PreTokenBalances: nil, Rewards: nil, }, //Transaction: types.Transaction{ // Message: types.Message{ // AccountKeys: nil, // AddressTableLookups: nil, // Header: types.Header{}, // Instructions: nil, // RecentBlockHash: "", // }, // Signatures: nil, //}, //Version: nil, } meta := y.Transaction.GetMeta() yTx := y.Transaction.Transaction if meta.Err != nil && len(meta.Err.GetErr()) > 0 { // If the transaction has an error, we set the error in the Meta sTx.Meta.Err = ParseTransactionErrorFromGeyser(meta.Err.GetErr()) // sTx.Meta.Err = meta.Err.GetErr() } sTx.Meta.Fee = meta.Fee //sTx.Meta.InnerInstructions = meta.InnerInstructions if meta.ComputeUnitsConsumed != nil { sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed } for _, innerInstr := range meta.InnerInstructions { var instrs []Instruction for _, instr := range innerInstr.Instructions { instrs = append(instrs, Instruction{ ProgramIDIndex: int(instr.ProgramIdIndex), Accounts: func() []int { var out []int for i := range instr.Accounts { out = append(out, int(instr.Accounts[i])) } return out }(), Data: instr.Data, StackHeight: newInt(instr.StackHeight), }) } sTx.Meta.InnerInstructions = append(sTx.Meta.InnerInstructions, InnerInstructions{ Index: int(innerInstr.Index), Instructions: instrs, }) } sTx.Meta.LogMessages = meta.LogMessages sTx.Meta.PostBalances = meta.PostBalances sTx.Meta.PostTokenBalances = grpcTokenBalance(meta.PostTokenBalances) sTx.Meta.PreBalances = meta.PreBalances sTx.Meta.PreTokenBalances = grpcTokenBalance(meta.PreTokenBalances) sTx.Meta.Rewards = nil sTx.Meta.LoadedAddresses.Readonly = byteSlicesToKeySlices(meta.LoadedReadonlyAddresses) sTx.Meta.LoadedAddresses.Writable = byteSlicesToKeySlices(meta.LoadedWritableAddresses) // copy signatures for i := range yTx.Signatures { sTx.Transaction.Signatures = append(sTx.Transaction.Signatures, solana.SignatureFromBytes(yTx.Signatures[i])) } // copy message sTx.Transaction.Message = Message{ RecentBlockHash: solana.HashFromBytes(yTx.Message.RecentBlockhash).String(), } // copy message.AccountKeys //stopAt := len(yTx.Message.AccountKeys) - sTx.Message.NumLookups() stopAt := len(yTx.Message.AccountKeys) for accIndex, acc := range yTx.Message.AccountKeys { sTx.Transaction.Message.AccountKeys = append(sTx.Transaction.Message.AccountKeys, solana.PublicKeyFromBytes(acc)) if accIndex == stopAt-1 { break } } // copy message.Header sTx.Transaction.Message.Header = Header{ NumRequiredSignatures: int(yTx.Message.Header.NumRequiredSignatures), NumReadonlySignedAccounts: int(yTx.Message.Header.NumReadonlySignedAccounts), NumReadonlyUnsignedAccounts: int(yTx.Message.Header.NumReadonlyUnsignedAccounts), } // copy message.versioned if yTx.Message.Versioned { sTx.Version = solana.MessageVersionV0 } else { sTx.Version = solana.MessageVersionLegacy } // copy address table lookups { tables := map[solana.PublicKey]solana.PublicKeySlice{} writable := byteSlicesToKeySlices(meta.LoadedWritableAddresses) readonly := byteSlicesToKeySlices(meta.LoadedReadonlyAddresses) for _, addr := range yTx.Message.AddressTableLookups { sTx.Transaction.Message.AddressTableLookups = append(sTx.Transaction.Message.AddressTableLookups, solana.MessageAddressTableLookup{ AccountKey: solana.PublicKeyFromBytes(addr.AccountKey), WritableIndexes: addr.WritableIndexes, ReadonlyIndexes: addr.ReadonlyIndexes, }) numTakeWritable := len(addr.WritableIndexes) numTakeReadonly := len(addr.ReadonlyIndexes) tableKey := solana.PublicKeyFromBytes(addr.AccountKey) { // now need to rebuild the address table taking into account the indexes, and put the keys into the tables maxIndex := 0 for _, indexB := range addr.WritableIndexes { index := int(indexB) if index > maxIndex { maxIndex = index } } for _, indexB := range addr.ReadonlyIndexes { index := int(indexB) if index > maxIndex { maxIndex = index } } tables[tableKey] = make([]solana.PublicKey, maxIndex+1) } if numTakeWritable > 0 { writableForTable := writable[:numTakeWritable] for i, indexB := range addr.WritableIndexes { index := int(indexB) tables[tableKey][index] = writableForTable[i] } writable = writable[numTakeWritable:] } if numTakeReadonly > 0 { readableForTable := readonly[:numTakeReadonly] for i, indexB := range addr.ReadonlyIndexes { index := int(indexB) tables[tableKey][index] = readableForTable[i] } readonly = readonly[numTakeReadonly:] } } } // copy instructions for _, instr := range yTx.Message.Instructions { sTx.Transaction.Message.Instructions = append(sTx.Transaction.Message.Instructions, Instruction{ ProgramIDIndex: int(instr.ProgramIdIndex), Accounts: func() []int { var out []int for i := range instr.Accounts { out = append(out, int(instr.Accounts[i])) } return out }(), Data: instr.Data, }) } // resolve the lookups //{ // if sTx.Transaction.Message.IsVersioned() { // // only versioned transactions have address table lookups // err := sTx.Transaction.Message.ResolveLookups() // if err != nil { // return sTx, fmt.Errorf("failed to resolve lookups: %w", err) // } // } //} return sTx, nil } func newInt16(x uint16) *int { y := int(x) return &y } func newInt(x *uint32) *int { if x == nil { return nil } y := int(*x) return &y } func byteSlicesToKeySlices(keys [][]byte) []solana.PublicKey { var out []solana.PublicKey for _, key := range keys { var k solana.PublicKey copy(k[:], key) out = append(out, k) } return out } func grpcTokenBalance(src []*pb.TokenBalance) []TokenBalance { out := make([]TokenBalance, len(src)) for i, tb := range src { var ( mintAccount solana.PublicKey ownerAccount solana.PublicKey programIDAccount solana.PublicKey ) if tb.Mint != "" { mintAccount, _ = solana.PublicKeyFromBase58(tb.Mint) } if tb.Owner != "" { ownerAccount, _ = solana.PublicKeyFromBase58(tb.Owner) } if tb.ProgramId != "" { programIDAccount, _ = solana.PublicKeyFromBase58(tb.ProgramId) } out[i] = TokenBalance{ AccountIndex: int(tb.AccountIndex), MintAccount: mintAccount, OwnerAccount: &ownerAccount, ProgramIDAccount: programIDAccount, Mint: tb.Mint, Owner: tb.Owner, ProgramID: tb.ProgramId, UITokenAmount: UITokenAmount{ Amount: tb.UiTokenAmount.Amount, Decimals: uint64(tb.UiTokenAmount.Decimals), UIAmount: tb.UiTokenAmount.UiAmount, UIAmountString: tb.UiTokenAmount.UiAmountString, }, } } return out }