448 lines
13 KiB
Go
448 lines
13 KiB
Go
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
|
|
}
|