Files
pump-parser/rawtx.go

1011 lines
30 KiB
Go
Raw Normal View History

2025-11-20 17:56:45 +08:00
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"
2025-12-22 17:56:40 +08:00
pb "go.onsig.ai/onsig/yellowstone-proto"
2025-11-20 17:56:45 +08:00
)
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"`
}
2025-12-08 16:35:40 +08:00
func (tx *RawTx) GetAccountLust() []solana.PublicKey {
return tx.getAccountList()
}
2025-11-20 17:56:45 +08:00
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 {
2026-02-11 17:49:43 +08:00
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"`
ComputeUnitsConsumed uint64 `json:"computeUnitsConsumed"`
2025-11-20 17:56:45 +08:00
}
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
}
2025-12-22 17:56:40 +08:00
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 {
e, _ := json.Marshal(meta.Err)
sTx.Meta.Err = string(e)
}
sTx.Meta.Fee = meta.Fee
//sTx.Meta.InnerInstructions = meta.InnerInstructions
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
}
2025-11-20 17:56:45 +08:00
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
}
2026-02-02 17:59:47 +08:00
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
}
2025-11-20 17:56:45 +08:00
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
}
2025-12-22 17:56:40 +08:00
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
}
}
2026-02-02 17:59:47 +08:00
var err error
2025-12-22 17:56:40 +08:00
if ataIndex == 0 {
2026-02-02 17:59:47 +08:00
ataIndex, err = getAtaIdxByOwner(result, result.accountList[accountIndex], mint)
if err != nil {
return decimal.Zero, ataIndex
}
2025-12-22 17:56:40 +08:00
}
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
}
2025-11-20 17:56:45 +08:00
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
}
}
2026-02-02 17:59:47 +08:00
var x *TokenBalance
var err error
2025-11-20 17:56:45 +08:00
if ataIndex == 0 {
2026-02-02 17:59:47 +08:00
x, err = getAtaByOwner(result, result.accountList[accountIndex], mint)
} else {
x, err = getTokenBalanceAfterTx(result, ataIndex)
2025-11-20 17:56:45 +08:00
}
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
}
2025-12-22 17:56:40 +08:00
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
transError, err := DecodeTransactionError(meta.Err.GetErr())
if err != nil {
sTx.Meta.Err = err
} else {
sTx.Meta.Err = transError
}
// sTx.Meta.Err = meta.Err.GetErr()
}
sTx.Meta.Fee = meta.Fee
//sTx.Meta.InnerInstructions = meta.InnerInstructions
2026-02-11 17:49:43 +08:00
sTx.Meta.ComputeUnitsConsumed = *meta.ComputeUnitsConsumed
2025-12-22 17:56:40 +08:00
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
}