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()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 16:11:34 +08:00
|
|
|
func (tx *RawTx) GetAccountList() []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-26 16:11:34 +08:00
|
|
|
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"`
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 16:11:34 +08:00
|
|
|
type RpcTransactionErr []interface{}
|
|
|
|
|
|
|
|
|
|
func marshalRpcTransactionErr(err any) string {
|
|
|
|
|
e, _ := json.Marshal(err)
|
|
|
|
|
if len(e) == 0 {
|
|
|
|
|
return "UnKnown"
|
|
|
|
|
}
|
|
|
|
|
return string(e)
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
2026-02-26 16:11:34 +08:00
|
|
|
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 {
|
2026-03-02 15:47:11 +08:00
|
|
|
if errDetail == "ComputationalBudgetExceeded" || errDetail == "ProgramFailedToComplete" {
|
2026-02-26 16:11:34 +08:00
|
|
|
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),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 17:56:40 +08:00
|
|
|
}
|
|
|
|
|
sTx.Meta.Fee = meta.Fee
|
|
|
|
|
//sTx.Meta.InnerInstructions = meta.InnerInstructions
|
2026-02-12 10:43:30 +08:00
|
|
|
if meta.ComputeUnitsConsumed != nil {
|
|
|
|
|
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: 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-12 10:43:30 +08:00
|
|
|
|
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
|
2026-02-26 16:11:34 +08:00
|
|
|
sTx.Meta.Err = ParseTransactionErrorFromGeyser(meta.Err.GetErr())
|
2025-12-22 17:56:40 +08:00
|
|
|
// sTx.Meta.Err = meta.Err.GetErr()
|
|
|
|
|
}
|
|
|
|
|
sTx.Meta.Fee = meta.Fee
|
|
|
|
|
//sTx.Meta.InnerInstructions = meta.InnerInstructions
|
2026-03-23 20:20:21 +08:00
|
|
|
if meta.ComputeUnitsConsumed != nil {
|
|
|
|
|
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
|
|
|
|
|
}
|