punm parser

This commit is contained in:
thloyi
2025-11-20 17:56:45 +08:00
commit a945f3b45d
29 changed files with 9665 additions and 0 deletions

443
rawtx.go Normal file
View File

@@ -0,0 +1,443 @@
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) 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
}