punm parser
This commit is contained in:
443
rawtx.go
Normal file
443
rawtx.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user