punm parser

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

7
example/geyser/Makefile Normal file
View File

@@ -0,0 +1,7 @@
protoc:
protoc \
--go_out=./proto \
--go_opt=paths=source_relative \
--go-grpc_out=./proto \
--go-grpc_opt=paths=source_relative \
--proto_path ./proto/ ./proto/*.proto

View File

@@ -0,0 +1,94 @@
package main
import (
"context"
"fmt"
"time"
parser "github.com/thloyi/pump-parser"
example "github.com/thloyi/pump-parser/example"
"github.com/thloyi/pump-parser/example/geyser"
)
func main() {
//pool, err := ants.NewPool(100, ants.WithPreAlloc(true), ants.WithNonblocking(true))
//if err != nil {
// panic(err)
//}
//xt := tracker.NewTwitterTracker(nil) // Initialize Twitter tracker if needed
// laserstream-mainnet-slc.helius-rpc.com:80
ch := make(chan geyser.SubscriptionMessage, 1)
go geyser.RunLoopWithReConnect(context.Background(), "127.0.0.1:10001", parser.SolProgramPump, ch)
// var tokenTxs = make(map[string]*types.Tx)
currentBlock := uint64(0)
for msg := range ch {
if msg.Tx == nil {
block := msg.Block
if block.Slot%100 == 0 {
fmt.Printf("slot: %d, hash: %s, time: %s, height: %d, estimate delay second: %d\n",
block.Slot, block.BlockHash, time.Unix(block.BlockTime, 0).Format("2006-01-02 15:04:05"), block.Height, msg.EstimateDelaySecond)
}
continue
}
ptx := msg.Tx
//data, _ := json.Marshal(tx)
//fmt.Println(string(data))
//continue
//if tx.Token0Address != "HRHLDjqFBhNeyTXUuZQE9gTy5z2112qeQBS9U79NHyyp" {
// continue
//}
//if tx.Program != parser.SolProgramPump {
// continue
//}
if currentBlock == ptx.Block {
continue
}
// 处理交易
txErr, ok := ptx.Err.(*geyser.TransactionError)
var customerErrCode uint32
var instructorErrIndex uint8
if ok {
instructorErrIndex, customerErrCode, _ = txErr.GetCustomErrorCode()
fmt.Printf("now: %s, block: %d, tx: %s, errInstr Code: %d, errInstrIndex: %d, err: %v\n", time.Now().Format("2006-01-02 15:04:05"), ptx.Block, ptx.GetTxHash(), customerErrCode, instructorErrIndex, ptx.Err)
} else {
txs := example.FromTx(ptx, msg.RawTx)
if len(txs) == 0 {
fmt.Printf("tx is empty, block: %d, tx %s \n", ptx.Block, ptx.GetTxHash())
continue
}
printed := false
for _, tx := range txs {
if tx.Program != parser.SolProgramPump {
continue
}
printed = true
fmt.Printf("t: %s, block: %d, hash: %s, signer: %s, program: %s, event: %s, token1: %s, cuPrice: %s, mevAgent: %s, mevFee: %s, platform: %s, platformFee: %s, entryContract: %s, mayhem: %t\n",
time.Now().Format(time.RFC3339Nano),
tx.Block, tx.GetTxHash(), tx.Maker, tx.Program, tx.Event, tx.Token1Amount, tx.CUPrice, tx.MevAgent, tx.MevAgentFee, tx.Platform, tx.PlatformFee, tx.EntryContract, tx.Mayhem)
break
}
if !printed {
continue
}
//fmt.Printf("t: %s, block: %d, hash: %s, signer: %s, program: %s, event: %s, token0: %s, token1: %s, signer before sol :%s, after sol: %s, after token: %s, tokencreator: %s, tokenprogram: %s, mayhem: %t\n",
// time.Now().Format(time.RFC3339Nano),
// tx.Block, tx.GetTxHash(), tx.Maker, tx.Program, tx.Event, tx.Token0Amount.String(), tx.Token1Amount.String(),
// tx.BeforeSolBalance, tx.AfterSOLBalance, tx.AfterSignerToken0Balance, tx.TokenCreator, tx.Token0Program, tx.Mayhem)
}
currentBlock = ptx.Block
//
//if tx.Event == "create" {
// if err := pool.Submit(func() {
// now := time.Now()
// xt.AddToken(tx.Token)
// log.Printf("Add token %s, cost: %s %s %v %v", tx.Token.Address, time.Since(now), tx.Token.Twitter, xt.DuplicateCount(tx.Token.Address), xt.HasTwitter(tx.Token.Address))
// }); err != nil {
// fmt.Println(err)
// }
//}
}
}

277
example/geyser/error.go Normal file
View File

@@ -0,0 +1,277 @@
package geyser
import (
"encoding/binary"
"errors"
)
type TransactionErrorVariant uint32
type InstructionErrorVariant uint32
type InstructionErrorEnum struct {
Index uint8
Variant InstructionErrorVariant
rest []byte
}
type CustomInstructionErrorEnum struct {
Code uint32
}
const (
AccountInUse TransactionErrorVariant = iota
AccountLoadedTwice
AccountNotFound
ProgramAccountNotFound
InsufficientFundsForFee
InvalidAccountForFee
AlreadyProcessed
BlockhashNotFound
InstructionError //InstructionError(u8, InstructionError),
CallChainTooDeep
MissingSignatureForFee
InvalidAccountIndex
SignatureFailure
InvalidProgramForExecution
SanitizeFailure
ClusterMaintenance
AccountBorrowOutstanding
WouldExceedMaxBlockCostLimit
UnsupportedVersion
InvalidWritableAccount
WouldExceedMaxAccountCostLimit
WouldExceedAccountDataBlockLimit
TooManyAccountLocks
AddressLookupTableNotFound
InvalidAddressLookupTableOwner
InvalidAddressLookupTableData
InvalidAddressLookupTableIndex
InvalidRentPayingAccount
WouldExceedMaxVoteCostLimit
WouldExceedAccountDataTotalLimit
DuplicateInstruction //DuplicateInstruction(u8),
/*
InsufficientFundsForRent {
account_index: u8,
},
*/
InsufficientFundsForRent
MaxLoadedAccountsDataSizeExceeded
InvalidLoadedAccountsDataSizeLimit
ResanitizationNeeded
/*
ProgramExecutionTemporarilyRestricted {
account_index: u8,
},
*/
ProgramExecutionTemporarilyRestricted
UnbalancedTransaction
ProgramCacheHitMaxLimit
CommitCancelled
)
const (
GenericError InstructionErrorVariant = iota
/// The arguments provided to a program were invalid
InvalidArgument
/// An instruction's data contents were invalid
InvalidInstructionData
/// An account's data contents was invalid
InvalidAccountData
/// An account's data was too small
AccountDataTooSmall
/// An account's balance was too small to complete the instruction
InsufficientFunds
/// The account did not have the expected program id
IncorrectProgramId
/// A signature was required but not found
MissingRequiredSignature
/// An initialize instruction was sent to an account that has already been initialized.
AccountAlreadyInitialized
/// An attempt to operate on an account that hasn't been initialized.
UninitializedAccount
/// Program's instruction lamport balance does not equal the balance after the instruction
UnbalancedInstruction
/// Program illegally modified an account's program id
ModifiedProgramId
/// Program spent the lamports of an account that doesn't belong to it
ExternalAccountLamportSpend
/// Program modified the data of an account that doesn't belong to it
ExternalAccountDataModified
/// Read-only account's lamports modified
ReadonlyLamportChange
/// Read-only account's data was modified
ReadonlyDataModified
/// An account was referenced more than once in a single instruction
// Deprecated, instructions can now contain duplicate accounts
DuplicateAccountIndex
/// Executable bit on account changed, but shouldn't have
ExecutableModified
/// Rent_epoch account changed, but shouldn't have
RentEpochModified
/// The instruction expected additional account keys
NotEnoughAccountKeys
/// Program other than the account's owner changed the size of the account data
AccountDataSizeChanged
/// The instruction expected an executable account
AccountNotExecutable
/// Failed to borrow a reference to account data, already borrowed
AccountBorrowFailed
/// Account data has an outstanding reference after a program's execution
InstructionAccountBorrowOutstanding
/// The same account was multiply passed to an on-chain program's entrypoint, but the program
/// modified them differently. A program can only modify one instance of the account because
/// the runtime cannot determine which changes to pick or how to merge them if both are modified
DuplicateAccountOutOfSync
/// Allows on-chain programs to implement program-specific error types and see them returned
/// by the Solana runtime. A program-specific error may be any type that is represented as
/// or serialized to a u32 integer.
Custom // Custom(u32),
/// The return value from the program was invalid. Valid errors are either a defined builtin
/// error value or a user-defined error in the lower 32 bits.
InvalidError
/// Executable account's data was modified
ExecutableDataModified
/// Executable account's lamports modified
ExecutableLamportChange
/// Executable accounts must be rent exempt
ExecutableAccountNotRentExempt
/// Unsupported program id
UnsupportedProgramId
/// Cross-program invocation call depth too deep
CallDepth
/// An account required by the instruction is missing
MissingAccount
/// Cross-program invocation reentrancy not allowed for this instruction
ReentrancyNotAllowed
/// Length of the seed is too long for address generation
MaxSeedLengthExceeded
/// Provided seeds do not result in a valid address
InvalidSeeds
/// Failed to reallocate account data of this length
InvalidRealloc
/// Computational budget exceeded
ComputationalBudgetExceeded
/// Cross-program invocation with unauthorized signer or writable account
PrivilegeEscalation
/// Failed to create program execution environment
ProgramEnvironmentSetupFailure
/// Program failed to complete
ProgramFailedToComplete
/// Program failed to compile
ProgramFailedToCompile
/// Account is immutable
Immutable
/// Incorrect authority provided
IncorrectAuthority
/// Failed to serialize or deserialize account data
///
/// Warning: This error should never be emitted by the runtime.
///
/// This error includes strings from the underlying 3rd party Borsh crate
/// which can be dangerous because the error strings could change across
/// Borsh versions. Only programs can use this error because they are
/// consistent across Solana software versions.
/// string values from this error should not be used in
BorshIoError // BorshIoError(String)
// An account does not have enough lamports to be rent-exempt
AccountNotRentExempt
/// Invalid account owner
InvalidAccountOwner
/// Program arithmetic overflowed
ArithmeticOverflow
/// Unsupported sysvar
UnsupportedSysvar
/// Illegal account owner
IllegalOwner
/// Accounts data allocations exceeded the maximum allowed per transaction
MaxAccountsDataAllocationsExceeded
/// Max accounts exceeded
MaxAccountsExceeded
/// Max instruction trace length exceeded
MaxInstructionTraceLengthExceeded
/// Builtin programs must consume compute units
BuiltinProgramsMustConsumeComputeUnits
)
type TransactionError struct {
Variant TransactionErrorVariant
rest []byte
}
var (
ErrInvalidTransactionError = errors.New("invalid transaction error")
NotAnInstructionError = errors.New("not an instruction error")
NotACustomInstructionError = errors.New("not a custom instruction error")
UnsupportedInstructionError = errors.New("unsupported instruction error")
)
func DecodeTransactionError(data []byte) (*TransactionError, error) {
if len(data) < 4 {
return nil, ErrInvalidTransactionError
}
var err TransactionError
variant := binary.LittleEndian.Uint32(data[:4])
if variant > uint32(CommitCancelled) {
return nil, UnsupportedInstructionError
}
err.Variant = TransactionErrorVariant(variant)
err.rest = data[4:]
return &err, nil
}
func (e *TransactionError) GetCustomErrorCode() (uint8, uint32, error) {
instr, err := e.GetInstructionError()
if err != nil {
return 0, 0, err
}
custom, err := instr.Custom()
if err != nil {
return 0, 0, err
}
return instr.Index, custom.Code, nil
}
func (e *TransactionError) GetInstructionError() (*InstructionErrorEnum, error) {
if e.Variant != InstructionError {
return nil, NotAnInstructionError
}
if len(e.rest) < 5 {
return nil, NotAnInstructionError
}
var err InstructionErrorEnum
err.Index = e.rest[0]
variant := binary.LittleEndian.Uint32(e.rest[1:5])
if variant > uint32(BuiltinProgramsMustConsumeComputeUnits) {
return nil, UnsupportedInstructionError
}
err.Variant = InstructionErrorVariant(binary.LittleEndian.Uint32(e.rest[1:5]))
err.rest = e.rest[5:]
return &err, nil
}
func (e *InstructionErrorEnum) Custom() (*CustomInstructionErrorEnum, error) {
if e.Variant != Custom {
return nil, NotACustomInstructionError
}
if len(e.rest) < 4 {
return nil, NotACustomInstructionError
}
var custom CustomInstructionErrorEnum
custom.Code = binary.LittleEndian.Uint32(e.rest[:4])
return &custom, nil
}

22
example/geyser/message.go Normal file
View File

@@ -0,0 +1,22 @@
package geyser
import (
"github.com/thloyi/pump-parser"
)
type BlockInfo struct {
Slot uint64
BlockTime int64
BlockHash string
Height uint64
}
type SubscriptionMessage struct {
EstimateDelaySecond int64
Reconnect bool
Block *BlockInfo
Tx *pump_parser.Tx
RawTx *pump_parser.RawTx
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,272 @@
syntax = "proto3";
import "google/protobuf/timestamp.proto";
import public "solana-storage.proto";
option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto";
package geyser;
service Geyser {
rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeUpdate) {}
rpc Ping(PingRequest) returns (PongResponse) {}
rpc GetLatestBlockhash(GetLatestBlockhashRequest) returns (GetLatestBlockhashResponse) {}
rpc GetBlockHeight(GetBlockHeightRequest) returns (GetBlockHeightResponse) {}
rpc GetSlot(GetSlotRequest) returns (GetSlotResponse) {}
rpc IsBlockhashValid(IsBlockhashValidRequest) returns (IsBlockhashValidResponse) {}
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {}
}
enum CommitmentLevel {
PROCESSED = 0;
CONFIRMED = 1;
FINALIZED = 2;
}
enum SlotStatus {
SLOT_PROCESSED = 0;
SLOT_CONFIRMED = 1;
SLOT_FINALIZED = 2;
SLOT_FIRST_SHRED_RECEIVED = 3;
SLOT_COMPLETED = 4;
SLOT_CREATED_BANK = 5;
SLOT_DEAD = 6;
}
message SubscribeRequest {
map<string, SubscribeRequestFilterAccounts> accounts = 1;
map<string, SubscribeRequestFilterSlots> slots = 2;
map<string, SubscribeRequestFilterTransactions> transactions = 3;
map<string, SubscribeRequestFilterTransactions> transactions_status = 10;
map<string, SubscribeRequestFilterBlocks> blocks = 4;
map<string, SubscribeRequestFilterBlocksMeta> blocks_meta = 5;
map<string, SubscribeRequestFilterEntry> entry = 8;
optional CommitmentLevel commitment = 6;
repeated SubscribeRequestAccountsDataSlice accounts_data_slice = 7;
optional SubscribeRequestPing ping = 9;
optional uint64 from_slot = 11;
}
message SubscribeRequestFilterAccounts {
repeated string account = 2;
repeated string owner = 3;
repeated SubscribeRequestFilterAccountsFilter filters = 4;
optional bool nonempty_txn_signature = 5;
}
message SubscribeRequestFilterAccountsFilter {
oneof filter {
SubscribeRequestFilterAccountsFilterMemcmp memcmp = 1;
uint64 datasize = 2;
bool token_account_state = 3;
SubscribeRequestFilterAccountsFilterLamports lamports = 4;
}
}
message SubscribeRequestFilterAccountsFilterMemcmp {
uint64 offset = 1;
oneof data {
bytes bytes = 2;
string base58 = 3;
string base64 = 4;
}
}
message SubscribeRequestFilterAccountsFilterLamports {
oneof cmp {
uint64 eq = 1;
uint64 ne = 2;
uint64 lt = 3;
uint64 gt = 4;
}
}
message SubscribeRequestFilterSlots {
optional bool filter_by_commitment = 1;
optional bool interslot_updates = 2;
}
message SubscribeRequestFilterTransactions {
optional bool vote = 1;
optional bool failed = 2;
optional string signature = 5;
repeated string account_include = 3;
repeated string account_exclude = 4;
repeated string account_required = 6;
}
message SubscribeRequestFilterBlocks {
repeated string account_include = 1;
optional bool include_transactions = 2;
optional bool include_accounts = 3;
optional bool include_entries = 4;
}
message SubscribeRequestFilterBlocksMeta {}
message SubscribeRequestFilterEntry {}
message SubscribeRequestAccountsDataSlice {
uint64 offset = 1;
uint64 length = 2;
}
message SubscribeRequestPing {
int32 id = 1;
}
message SubscribeUpdate {
repeated string filters = 1;
oneof update_oneof {
SubscribeUpdateAccount account = 2;
SubscribeUpdateSlot slot = 3;
SubscribeUpdateTransaction transaction = 4;
SubscribeUpdateTransactionStatus transaction_status = 10;
SubscribeUpdateBlock block = 5;
SubscribeUpdatePing ping = 6;
SubscribeUpdatePong pong = 9;
SubscribeUpdateBlockMeta block_meta = 7;
SubscribeUpdateEntry entry = 8;
}
google.protobuf.Timestamp created_at = 11;
}
message SubscribeUpdateAccount {
SubscribeUpdateAccountInfo account = 1;
uint64 slot = 2;
bool is_startup = 3;
}
message SubscribeUpdateAccountInfo {
bytes pubkey = 1;
uint64 lamports = 2;
bytes owner = 3;
bool executable = 4;
uint64 rent_epoch = 5;
bytes data = 6;
uint64 write_version = 7;
optional bytes txn_signature = 8;
}
message SubscribeUpdateSlot {
uint64 slot = 1;
optional uint64 parent = 2;
SlotStatus status = 3;
optional string dead_error = 4;
}
message SubscribeUpdateTransaction {
SubscribeUpdateTransactionInfo transaction = 1;
uint64 slot = 2;
}
message SubscribeUpdateTransactionInfo {
bytes signature = 1;
bool is_vote = 2;
solana.storage.ConfirmedBlock.Transaction transaction = 3;
solana.storage.ConfirmedBlock.TransactionStatusMeta meta = 4;
uint64 index = 5;
}
message SubscribeUpdateTransactionStatus {
uint64 slot = 1;
bytes signature = 2;
bool is_vote = 3;
uint64 index = 4;
solana.storage.ConfirmedBlock.TransactionError err = 5;
}
message SubscribeUpdateBlock {
uint64 slot = 1;
string blockhash = 2;
solana.storage.ConfirmedBlock.Rewards rewards = 3;
solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4;
solana.storage.ConfirmedBlock.BlockHeight block_height = 5;
uint64 parent_slot = 7;
string parent_blockhash = 8;
uint64 executed_transaction_count = 9;
repeated SubscribeUpdateTransactionInfo transactions = 6;
uint64 updated_account_count = 10;
repeated SubscribeUpdateAccountInfo accounts = 11;
uint64 entries_count = 12;
repeated SubscribeUpdateEntry entries = 13;
}
message SubscribeUpdateBlockMeta {
uint64 slot = 1;
string blockhash = 2;
solana.storage.ConfirmedBlock.Rewards rewards = 3;
solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4;
solana.storage.ConfirmedBlock.BlockHeight block_height = 5;
uint64 parent_slot = 6;
string parent_blockhash = 7;
uint64 executed_transaction_count = 8;
uint64 entries_count = 9;
}
message SubscribeUpdateEntry {
uint64 slot = 1;
uint64 index = 2;
uint64 num_hashes = 3;
bytes hash = 4;
uint64 executed_transaction_count = 5;
uint64 starting_transaction_index = 6; // added in v1.18, for solana 1.17 value is always 0
}
message SubscribeUpdatePing {}
message SubscribeUpdatePong {
int32 id = 1;
}
// non-streaming methods
message PingRequest {
int32 count = 1;
}
message PongResponse {
int32 count = 1;
}
message GetLatestBlockhashRequest {
optional CommitmentLevel commitment = 1;
}
message GetLatestBlockhashResponse {
uint64 slot = 1;
string blockhash = 2;
uint64 last_valid_block_height = 3;
}
message GetBlockHeightRequest {
optional CommitmentLevel commitment = 1;
}
message GetBlockHeightResponse {
uint64 block_height = 1;
}
message GetSlotRequest {
optional CommitmentLevel commitment = 1;
}
message GetSlotResponse {
uint64 slot = 1;
}
message GetVersionRequest {}
message GetVersionResponse {
string version = 1;
}
message IsBlockhashValidRequest {
string blockhash = 1;
optional CommitmentLevel commitment = 2;
}
message IsBlockhashValidResponse {
uint64 slot = 1;
bool valid = 2;
}

View File

@@ -0,0 +1,344 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// source: geyser.proto
package proto
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Geyser_Subscribe_FullMethodName = "/geyser.Geyser/Subscribe"
Geyser_Ping_FullMethodName = "/geyser.Geyser/Ping"
Geyser_GetLatestBlockhash_FullMethodName = "/geyser.Geyser/GetLatestBlockhash"
Geyser_GetBlockHeight_FullMethodName = "/geyser.Geyser/GetBlockHeight"
Geyser_GetSlot_FullMethodName = "/geyser.Geyser/GetSlot"
Geyser_IsBlockhashValid_FullMethodName = "/geyser.Geyser/IsBlockhashValid"
Geyser_GetVersion_FullMethodName = "/geyser.Geyser/GetVersion"
)
// GeyserClient is the client API for Geyser service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type GeyserClient interface {
Subscribe(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SubscribeRequest, SubscribeUpdate], error)
Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PongResponse, error)
GetLatestBlockhash(ctx context.Context, in *GetLatestBlockhashRequest, opts ...grpc.CallOption) (*GetLatestBlockhashResponse, error)
GetBlockHeight(ctx context.Context, in *GetBlockHeightRequest, opts ...grpc.CallOption) (*GetBlockHeightResponse, error)
GetSlot(ctx context.Context, in *GetSlotRequest, opts ...grpc.CallOption) (*GetSlotResponse, error)
IsBlockhashValid(ctx context.Context, in *IsBlockhashValidRequest, opts ...grpc.CallOption) (*IsBlockhashValidResponse, error)
GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error)
}
type geyserClient struct {
cc grpc.ClientConnInterface
}
func NewGeyserClient(cc grpc.ClientConnInterface) GeyserClient {
return &geyserClient{cc}
}
func (c *geyserClient) Subscribe(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[SubscribeRequest, SubscribeUpdate], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &Geyser_ServiceDesc.Streams[0], Geyser_Subscribe_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[SubscribeRequest, SubscribeUpdate]{ClientStream: stream}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Geyser_SubscribeClient = grpc.BidiStreamingClient[SubscribeRequest, SubscribeUpdate]
func (c *geyserClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PongResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(PongResponse)
err := c.cc.Invoke(ctx, Geyser_Ping_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *geyserClient) GetLatestBlockhash(ctx context.Context, in *GetLatestBlockhashRequest, opts ...grpc.CallOption) (*GetLatestBlockhashResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetLatestBlockhashResponse)
err := c.cc.Invoke(ctx, Geyser_GetLatestBlockhash_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *geyserClient) GetBlockHeight(ctx context.Context, in *GetBlockHeightRequest, opts ...grpc.CallOption) (*GetBlockHeightResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetBlockHeightResponse)
err := c.cc.Invoke(ctx, Geyser_GetBlockHeight_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *geyserClient) GetSlot(ctx context.Context, in *GetSlotRequest, opts ...grpc.CallOption) (*GetSlotResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetSlotResponse)
err := c.cc.Invoke(ctx, Geyser_GetSlot_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *geyserClient) IsBlockhashValid(ctx context.Context, in *IsBlockhashValidRequest, opts ...grpc.CallOption) (*IsBlockhashValidResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(IsBlockhashValidResponse)
err := c.cc.Invoke(ctx, Geyser_IsBlockhashValid_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *geyserClient) GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetVersionResponse)
err := c.cc.Invoke(ctx, Geyser_GetVersion_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// GeyserServer is the server API for Geyser service.
// All implementations must embed UnimplementedGeyserServer
// for forward compatibility.
type GeyserServer interface {
Subscribe(grpc.BidiStreamingServer[SubscribeRequest, SubscribeUpdate]) error
Ping(context.Context, *PingRequest) (*PongResponse, error)
GetLatestBlockhash(context.Context, *GetLatestBlockhashRequest) (*GetLatestBlockhashResponse, error)
GetBlockHeight(context.Context, *GetBlockHeightRequest) (*GetBlockHeightResponse, error)
GetSlot(context.Context, *GetSlotRequest) (*GetSlotResponse, error)
IsBlockhashValid(context.Context, *IsBlockhashValidRequest) (*IsBlockhashValidResponse, error)
GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error)
mustEmbedUnimplementedGeyserServer()
}
// UnimplementedGeyserServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedGeyserServer struct{}
func (UnimplementedGeyserServer) Subscribe(grpc.BidiStreamingServer[SubscribeRequest, SubscribeUpdate]) error {
return status.Errorf(codes.Unimplemented, "method Subscribe not implemented")
}
func (UnimplementedGeyserServer) Ping(context.Context, *PingRequest) (*PongResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented")
}
func (UnimplementedGeyserServer) GetLatestBlockhash(context.Context, *GetLatestBlockhashRequest) (*GetLatestBlockhashResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetLatestBlockhash not implemented")
}
func (UnimplementedGeyserServer) GetBlockHeight(context.Context, *GetBlockHeightRequest) (*GetBlockHeightResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetBlockHeight not implemented")
}
func (UnimplementedGeyserServer) GetSlot(context.Context, *GetSlotRequest) (*GetSlotResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetSlot not implemented")
}
func (UnimplementedGeyserServer) IsBlockhashValid(context.Context, *IsBlockhashValidRequest) (*IsBlockhashValidResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method IsBlockhashValid not implemented")
}
func (UnimplementedGeyserServer) GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetVersion not implemented")
}
func (UnimplementedGeyserServer) mustEmbedUnimplementedGeyserServer() {}
func (UnimplementedGeyserServer) testEmbeddedByValue() {}
// UnsafeGeyserServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to GeyserServer will
// result in compilation errors.
type UnsafeGeyserServer interface {
mustEmbedUnimplementedGeyserServer()
}
func RegisterGeyserServer(s grpc.ServiceRegistrar, srv GeyserServer) {
// If the following call pancis, it indicates UnimplementedGeyserServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Geyser_ServiceDesc, srv)
}
func _Geyser_Subscribe_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(GeyserServer).Subscribe(&grpc.GenericServerStream[SubscribeRequest, SubscribeUpdate]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Geyser_SubscribeServer = grpc.BidiStreamingServer[SubscribeRequest, SubscribeUpdate]
func _Geyser_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PingRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeyserServer).Ping(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Geyser_Ping_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeyserServer).Ping(ctx, req.(*PingRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Geyser_GetLatestBlockhash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetLatestBlockhashRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeyserServer).GetLatestBlockhash(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Geyser_GetLatestBlockhash_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeyserServer).GetLatestBlockhash(ctx, req.(*GetLatestBlockhashRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Geyser_GetBlockHeight_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetBlockHeightRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeyserServer).GetBlockHeight(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Geyser_GetBlockHeight_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeyserServer).GetBlockHeight(ctx, req.(*GetBlockHeightRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Geyser_GetSlot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetSlotRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeyserServer).GetSlot(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Geyser_GetSlot_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeyserServer).GetSlot(ctx, req.(*GetSlotRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Geyser_IsBlockhashValid_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(IsBlockhashValidRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeyserServer).IsBlockhashValid(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Geyser_IsBlockhashValid_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeyserServer).IsBlockhashValid(ctx, req.(*IsBlockhashValidRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Geyser_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetVersionRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GeyserServer).GetVersion(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Geyser_GetVersion_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GeyserServer).GetVersion(ctx, req.(*GetVersionRequest))
}
return interceptor(ctx, in, info, handler)
}
// Geyser_ServiceDesc is the grpc.ServiceDesc for Geyser service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Geyser_ServiceDesc = grpc.ServiceDesc{
ServiceName: "geyser.Geyser",
HandlerType: (*GeyserServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Ping",
Handler: _Geyser_Ping_Handler,
},
{
MethodName: "GetLatestBlockhash",
Handler: _Geyser_GetLatestBlockhash_Handler,
},
{
MethodName: "GetBlockHeight",
Handler: _Geyser_GetBlockHeight_Handler,
},
{
MethodName: "GetSlot",
Handler: _Geyser_GetSlot_Handler,
},
{
MethodName: "IsBlockhashValid",
Handler: _Geyser_IsBlockhashValid_Handler,
},
{
MethodName: "GetVersion",
Handler: _Geyser_GetVersion_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "Subscribe",
Handler: _Geyser_Subscribe_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "geyser.proto",
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,149 @@
syntax = "proto3";
package solana.storage.ConfirmedBlock;
option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto";
message ConfirmedBlock {
string previous_blockhash = 1;
string blockhash = 2;
uint64 parent_slot = 3;
repeated ConfirmedTransaction transactions = 4;
repeated Reward rewards = 5;
UnixTimestamp block_time = 6;
BlockHeight block_height = 7;
NumPartitions num_partitions = 8;
}
message ConfirmedTransaction {
Transaction transaction = 1;
TransactionStatusMeta meta = 2;
}
message Transaction {
repeated bytes signatures = 1;
Message message = 2;
}
message Message {
MessageHeader header = 1;
repeated bytes account_keys = 2;
bytes recent_blockhash = 3;
repeated CompiledInstruction instructions = 4;
bool versioned = 5;
repeated MessageAddressTableLookup address_table_lookups = 6;
}
message MessageHeader {
uint32 num_required_signatures = 1;
uint32 num_readonly_signed_accounts = 2;
uint32 num_readonly_unsigned_accounts = 3;
}
message MessageAddressTableLookup {
bytes account_key = 1;
bytes writable_indexes = 2;
bytes readonly_indexes = 3;
}
message TransactionStatusMeta {
TransactionError err = 1;
uint64 fee = 2;
repeated uint64 pre_balances = 3;
repeated uint64 post_balances = 4;
repeated InnerInstructions inner_instructions = 5;
bool inner_instructions_none = 10;
repeated string log_messages = 6;
bool log_messages_none = 11;
repeated TokenBalance pre_token_balances = 7;
repeated TokenBalance post_token_balances = 8;
repeated Reward rewards = 9;
repeated bytes loaded_writable_addresses = 12;
repeated bytes loaded_readonly_addresses = 13;
ReturnData return_data = 14;
bool return_data_none = 15;
// Sum of compute units consumed by all instructions.
// Available since Solana v1.10.35 / v1.11.6.
// Set to `None` for txs executed on earlier versions.
optional uint64 compute_units_consumed = 16;
}
message TransactionError {
bytes err = 1;
}
message InnerInstructions {
uint32 index = 1;
repeated InnerInstruction instructions = 2;
}
message InnerInstruction {
uint32 program_id_index = 1;
bytes accounts = 2;
bytes data = 3;
// Invocation stack height of an inner instruction.
// Available since Solana v1.14.6
// Set to `None` for txs executed on earlier versions.
optional uint32 stack_height = 4;
}
message CompiledInstruction {
uint32 program_id_index = 1;
bytes accounts = 2;
bytes data = 3;
}
message TokenBalance {
uint32 account_index = 1;
string mint = 2;
UiTokenAmount ui_token_amount = 3;
string owner = 4;
string program_id = 5;
}
message UiTokenAmount {
double ui_amount = 1;
uint32 decimals = 2;
string amount = 3;
string ui_amount_string = 4;
}
message ReturnData {
bytes program_id = 1;
bytes data = 2;
}
enum RewardType {
Unspecified = 0;
Fee = 1;
Rent = 2;
Staking = 3;
Voting = 4;
}
message Reward {
string pubkey = 1;
int64 lamports = 2;
uint64 post_balance = 3;
RewardType reward_type = 4;
string commission = 5;
}
message Rewards {
repeated Reward rewards = 1;
NumPartitions num_partitions = 2;
}
message UnixTimestamp {
int64 timestamp = 1;
}
message BlockHeight {
uint64 block_height = 1;
}
message NumPartitions {
uint64 num_partitions = 1;
}

60
example/geyser/pump.go Normal file
View File

@@ -0,0 +1,60 @@
package geyser
import (
"fmt"
"github.com/shopspring/decimal"
types "github.com/thloyi/pump-parser"
)
type PumpHandler struct {
callback func(*types.Tx, *types.RawTx)
}
func NewPumpHandler(cb func(*types.Tx, *types.RawTx)) *PumpHandler {
return &PumpHandler{
callback: func(tx *types.Tx, tx2 *types.RawTx) {
//tx.Check(tx2)
cb(tx, tx2)
},
}
}
func (h *PumpHandler) HandleMessage(rawTx *types.RawTx) {
if rawTx.Meta.Err != nil {
// Notify the channel about the failed transaction
beforeSolBalance := decimal.Zero
afterSolBalance := decimal.Zero
if rawTx.Meta.PreBalances != nil && len(rawTx.Meta.PreBalances) > 0 {
beforeSolBalance = decimal.NewFromUint64(rawTx.Meta.PreBalances[0]).Div(decimal.NewFromInt(1e9))
}
if rawTx.Meta.PostBalances != nil && len(rawTx.Meta.PostBalances) > 0 {
afterSolBalance = decimal.NewFromUint64(rawTx.Meta.PostBalances[0]).Div(decimal.NewFromInt(1e9))
}
h.callback(&types.Tx{
TxHash: (*[64]byte)((rawTx.Transaction.Signatures[0][:])),
Err: rawTx.Meta.Err,
Signer: rawTx.GetSigner(),
Block: rawTx.Slot,
BlockIndex: uint64(rawTx.IndexWithinBlock),
BeforeSolBalance: beforeSolBalance,
AfterSOLBalance: afterSolBalance,
}, rawTx)
return
}
parsedTx, err := types.Parser(rawTx)
if err != nil {
fmt.Printf("parser error: %s, block: %d tx: %s\n", err, rawTx.Slot, rawTx.TxHash())
return
}
if len(parsedTx.Swaps) == 0 {
// no swap, ignore
return
}
// fmt.Println(parsedTx.GetTxHash(), len(parsedTx.Swaps))
if h.callback != nil {
h.callback(parsedTx, rawTx)
}
}

View File

@@ -0,0 +1,527 @@
package geyser
import (
"context"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"log"
"time"
solana2 "github.com/gagliardetto/solana-go"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/metadata"
types "github.com/thloyi/pump-parser"
pb "github.com/thloyi/pump-parser/example/geyser/proto"
)
type Handler interface {
HandleMessage(rawTx *types.RawTx)
}
var kacp = keepalive.ClientParameters{
Time: 10 * time.Second, // send pings every 10 seconds if there is no activity
Timeout: time.Second, // wait 1 second for ping ack before considering the connection dead
PermitWithoutStream: true, // send pings even without active streams
}
type Client struct {
ch chan SubscriptionMessage
endpoint string
conn *grpc.ClientConn
ctx context.Context
lastReceiveTime time.Time
backoffFactor float64
subscription *pb.SubscribeRequest
subStatus bool
leastBlock BlockInfo
firstMessage bool
handler Handler
}
func NewClientWithPumpSwap(endpoint string, ch chan SubscriptionMessage) *Client {
var subscription pb.SubscribeRequest
var failed = false
var vote = false
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
Failed: &failed,
Vote: &vote,
}
subscription.Transactions["transactions_sub"].AccountInclude = []string{
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", //Pump AMM
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", //Pump
}
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
c := &Client{
backoffFactor: 1.5,
ch: ch,
endpoint: endpoint,
lastReceiveTime: time.Now(),
subStatus: false,
subscription: &subscription,
}
c.handler = NewPumpHandler(func(tx *types.Tx, tx2 *types.RawTx) {
c.sendTx(tx, tx2)
})
return c
}
func NewClientWithLaunchLab(endpoint string, ch chan SubscriptionMessage) *Client {
var subscription pb.SubscribeRequest
var failed = false
var vote = false
subscription.Transactions = make(map[string]*pb.SubscribeRequestFilterTransactions)
subscription.Transactions["transactions_sub"] = &pb.SubscribeRequestFilterTransactions{
Failed: &failed,
Vote: &vote,
}
subscription.Transactions["transactions_sub"].AccountInclude = []string{
"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", //LaunchLab
"CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C", //CPMM
//"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", //V4
}
subscription.BlocksMeta = make(map[string]*pb.SubscribeRequestFilterBlocksMeta)
subscription.BlocksMeta["block_meta"] = &pb.SubscribeRequestFilterBlocksMeta{}
c := &Client{
backoffFactor: 1.5,
ch: ch,
endpoint: endpoint,
lastReceiveTime: time.Now(),
subStatus: false,
subscription: &subscription,
}
c.handler = NewPumpHandler(func(tx *types.Tx, tx2 *types.RawTx) {
c.sendTx(tx, tx2)
})
return c
}
func RunLoopWithReConnect(ctx context.Context, endpoint, program string, ch chan SubscriptionMessage) {
var client *Client
if program == types.SolProgramRaydiumLaunchLab {
client = NewClientWithLaunchLab(endpoint, ch)
} else {
client = NewClientWithPumpSwap(endpoint, ch)
}
for {
select {
case <-ctx.Done():
log.Println("Context done, exiting loop")
return
default:
}
err := client.Connect(ctx)
if err != nil {
log.Printf("Failed to connect: %v", err)
time.Sleep(5 * time.Second)
continue
}
// should not reach here, because Connect will block
panic("geyser already connected, waiting for messages...")
}
}
func (c *Client) SetSubscribe(subscription *pb.SubscribeRequest) {
c.subscription = subscription
}
func (c *Client) Connect(ctx context.Context) error {
c.ctx = ctx
if c.conn == nil {
// 连接到 geyser
conn, err := c.grpcConnect(c.endpoint, true)
if err != nil {
return err
}
c.conn = conn
}
if c.subStatus {
return nil // 已经订阅了
}
// 订阅交易
err := c.grpcSubscribe(ctx, c.conn)
if err != nil {
if c.conn != nil {
c.conn.Close()
}
c.conn = nil
c.subStatus = false
log.Printf("Failed to subscribe: %v", err)
return err
}
return nil
}
func (c *Client) grpcConnect(address string, plaintext bool) (*grpc.ClientConn, error) {
var opts []grpc.DialOption
if plaintext {
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
} else {
pool, _ := x509.SystemCertPool()
creds := credentials.NewClientTLSFromCert(pool, "")
opts = append(opts, grpc.WithTransportCredentials(creds))
}
opts = append(opts, grpc.WithKeepaliveParams(kacp))
log.Println("Starting grpc client, connecting to", address)
conn, err := grpc.NewClient(address, opts...)
if err != nil {
return nil, fmt.Errorf("fail to dial: %v", err)
}
return conn, nil
}
func (c *Client) grpcSubscribe(ctx context.Context, conn *grpc.ClientConn) error {
var err error
client := pb.NewGeyserClient(conn)
//subscription.Transactions["transactions_sub"].AccountExclude = transactionsAccountsExclude
subscriptionJson, err := json.Marshal(c.subscription)
if err != nil {
log.Printf("Failed to marshal subscription request: %v", subscriptionJson)
return err
}
log.Printf("Subscription request: %s", string(subscriptionJson))
// Set up the subscription request
//if *token != "" {
// md := metadata.New(map[string]string{"x-token": *token})
// ctx = metadata.NewOutgoingContext(ctx, md)
//}
md := metadata.New(map[string]string{"x-token": "5adcf1f9-5719-43d1-bf3f-c2d4e1e5f94d"})
ctx = metadata.NewOutgoingContext(ctx, md)
stream, err := client.Subscribe(ctx)
if err != nil {
return err
}
err = stream.Send(c.subscription)
if err != nil {
return err
}
c.subStatus = true
c.firstMessage = true
for {
resp, err := stream.Recv()
if err == io.EOF {
return err
}
if err != nil {
return fmt.Errorf("error occurred in receiving update: %s", err)
}
txn := resp.GetTransaction()
if txn == nil {
blockMeta := resp.GetBlockMeta()
if blockMeta != nil && c.ch != nil {
c.sendBlock(blockMeta)
}
continue
}
rawTx, err := ConvertYellowstoneGrpcTransactionToSolanaTransaction(txn, resp.GetCreatedAt().Seconds)
if err != nil {
log.Printf("Failed to convert transaction: %v", err)
continue
}
c.handler.HandleMessage(rawTx)
}
}
func (c *Client) computeDelay(slot uint64) int64 {
if c.leastBlock.Slot == 0 {
return 0
}
if slot < c.leastBlock.Slot {
return 0
}
delay := time.Now().Unix() - c.leastBlock.BlockTime - (4 * int64(slot-c.leastBlock.Slot) / 10)
return delay
}
func (c *Client) sendTx(t *types.Tx, tx *types.RawTx) {
c.ch <- SubscriptionMessage{
Reconnect: c.firstMessage,
EstimateDelaySecond: c.computeDelay(tx.Slot),
Block: nil,
Tx: t,
RawTx: tx,
}
c.firstMessage = false
}
func (c *Client) sendBlock(blockMeta *pb.SubscribeUpdateBlockMeta) {
c.leastBlock.Slot = blockMeta.GetSlot()
c.leastBlock.BlockTime = blockMeta.GetBlockTime().Timestamp
c.leastBlock.BlockHash = blockMeta.Blockhash
c.leastBlock.Height = blockMeta.BlockHeight.BlockHeight
c.ch <- SubscriptionMessage{
EstimateDelaySecond: time.Now().Unix() - blockMeta.GetBlockTime().Timestamp,
Reconnect: c.firstMessage,
Block: &BlockInfo{
Slot: c.leastBlock.Slot,
BlockTime: c.leastBlock.BlockTime,
BlockHash: c.leastBlock.BlockHash,
Height: c.leastBlock.Height,
},
Tx: nil,
RawTx: nil,
}
c.firstMessage = false
}
func ConvertYellowstoneGrpcTransactionToSolanaTransaction(y *pb.SubscribeUpdateTransaction, created int64) (*types.RawTx, error) {
sTx := &types.RawTx{
BlockTime: created,
Slot: y.Slot,
IndexWithinBlock: int64(y.Transaction.Index),
Meta: types.Meta{
Err: nil,
Fee: 0,
InnerInstructions: nil,
LoadedAddresses: types.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
for _, innerInstr := range meta.InnerInstructions {
var instrs []types.Instruction
for _, instr := range innerInstr.Instructions {
instrs = append(instrs, types.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,
})
}
sTx.Meta.InnerInstructions = append(sTx.Meta.InnerInstructions, types.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, solana2.SignatureFromBytes(yTx.Signatures[i]))
}
// copy message
sTx.Transaction.Message = types.Message{
RecentBlockHash: solana2.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, solana2.PublicKeyFromBytes(acc))
if accIndex == stopAt-1 {
break
}
}
// copy message.Header
sTx.Transaction.Message.Header = types.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 = solana2.MessageVersionV0
} else {
sTx.Version = solana2.MessageVersionLegacy
}
// copy address table lookups
{
tables := map[solana2.PublicKey]solana2.PublicKeySlice{}
writable := byteSlicesToKeySlices(meta.LoadedWritableAddresses)
readonly := byteSlicesToKeySlices(meta.LoadedReadonlyAddresses)
for _, addr := range yTx.Message.AddressTableLookups {
sTx.Transaction.Message.AddressTableLookups = append(sTx.Transaction.Message.AddressTableLookups, solana2.MessageAddressTableLookup{
AccountKey: solana2.PublicKeyFromBytes(addr.AccountKey),
WritableIndexes: addr.WritableIndexes,
ReadonlyIndexes: addr.ReadonlyIndexes,
})
numTakeWritable := len(addr.WritableIndexes)
numTakeReadonly := len(addr.ReadonlyIndexes)
tableKey := solana2.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([]solana2.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, types.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 byteSlicesToKeySlices(keys [][]byte) []solana2.PublicKey {
var out []solana2.PublicKey
for _, key := range keys {
var k solana2.PublicKey
copy(k[:], key)
out = append(out, k)
}
return out
}
func grpcTokenBalance(src []*pb.TokenBalance) []types.TokenBalance {
out := make([]types.TokenBalance, len(src))
for i, tb := range src {
var (
mintAccount solana2.PublicKey
ownerAccount solana2.PublicKey
programIDAccount solana2.PublicKey
)
if tb.Mint != "" {
mintAccount, _ = solana2.PublicKeyFromBase58(tb.Mint)
}
if tb.Owner != "" {
ownerAccount, _ = solana2.PublicKeyFromBase58(tb.Owner)
}
if tb.ProgramId != "" {
programIDAccount, _ = solana2.PublicKeyFromBase58(tb.ProgramId)
}
out[i] = types.TokenBalance{
AccountIndex: int(tb.AccountIndex),
MintAccount: mintAccount,
OwnerAccount: &ownerAccount,
ProgramIDAccount: programIDAccount,
Mint: tb.Mint,
Owner: tb.Owner,
ProgramID: tb.ProgramId,
UITokenAmount: types.UITokenAmount{
Amount: tb.UiTokenAmount.Amount,
Decimals: uint64(tb.UiTokenAmount.Decimals),
UIAmount: tb.UiTokenAmount.UiAmount,
UIAmountString: tb.UiTokenAmount.UiAmountString,
},
}
}
return out
}

229
example/tx.go Normal file
View File

@@ -0,0 +1,229 @@
package example
import (
"time"
"github.com/gagliardetto/solana-go"
"github.com/jackc/pgtype"
"github.com/mr-tron/base58"
"github.com/shopspring/decimal"
parser "github.com/thloyi/pump-parser"
)
type Tx struct {
Err interface{} `json:"err,omitempty" gorm:"-"`
BondingCurve string `json:"bonding_curve" gorm:"-"`
PairAddress string `json:"pair_address"`
Maker string `json:"maker"`
Token0Address string `json:"token0_address"`
Token0Program string `json:"token0_program" gorm:"-"`
Token0Decimals uint8 `json:"token0_decimals" gorm:"-"`
Token1Address string `json:"token1_address"`
Token0Amount decimal.Decimal `json:"token0Amount" gorm:"column:token0_amount;type:numeric"`
Token1Amount decimal.Decimal `json:"token1Amount" gorm:"column:token1_amount;type:numeric"`
PriceUsd decimal.Decimal `json:"price_usd" gorm:"column:price_usd;type:numeric"`
AmountUsd decimal.Decimal `json:"amount_usd" gorm:"column:amount_usd;type:numeric"`
Block uint64 `json:"block"`
BlockIndex uint64 `json:"index"`
Event string `json:"event"`
CachedTxHash string `json:"tx_hash" gorm:"column:tx_hash"`
txHash *[64]byte `gorm:"-"`
TxIndex uint64 `json:"topic_index"`
Program string `json:"program"`
BlockAt pgtype.Timestamptz `gorm:"column:block_at;default:NULL" json:"block_at"`
CreatedAt *pgtype.Timestamptz `gorm:"autoCreateTime" json:"-"`
TotalSupply string `gorm:"total_supply"`
AfterReserve0 string `gorm:"after_reserve0"`
AfterReserve1 string `gorm:"after_reserve1"`
PositionChange int64 `gorm:"position_change"`
QuoteIsToken0 bool `gorm:"-"`
CurrentPrice decimal.Decimal `gorm:"-"`
TokenCreator string `gorm:"-"`
Platform string `gorm:"column:tx_platform;type:platform;default:'none'" json:"platform"`
MevAgent string `gorm:"column:tx_mev_agent;type:mev_agent;default:'none'" json:"tx_mev_agent"`
MevAgentFee decimal.Decimal `gorm:"column:tx_mev_agent_fee;type:numeric" json:"tx_mev_agent_fee"`
CUPrice decimal.Decimal `gorm:"column:tx_cu_price;type:numeric" json:"tx_cu_price"`
PlatformFee decimal.Decimal `gorm:"-"`
AfterSignerToken0Balance decimal.Decimal `gorm:"-" json:"-"`
BeforeSolBalance decimal.Decimal `gorm:"-" json:"-"`
AfterSOLBalance decimal.Decimal `gorm:"column:after_sol_balance;type:numeric" json:"after_sol_balance"`
EntryContract string `gorm:"column:tx_entry_contract;type:entry_contract;default:'none'" json:"tx_entry_contract"`
Mayhem bool
}
func (tx *Tx) GetTxHash() string {
if tx.CachedTxHash != "" {
return tx.CachedTxHash
}
if tx.txHash == nil {
return ""
}
tx.CachedTxHash = base58.Encode(tx.txHash[:])
return tx.CachedTxHash
}
func FromTx(tx *parser.Tx, raw *parser.RawTx) []*Tx {
var txs []*Tx = make([]*Tx, 0, len(tx.Swaps))
mev, mevFee := tx.CheckMevAgent()
for i, s := range tx.Swaps {
var newTx *Tx
platform, platformFee := tx.CheckPlatform(s, raw)
token0Program := s.BaseTokenProgram
token0Address := s.BaseMint
if s.Program == "Pump" {
newTx = &Tx{
Err: nil,
BondingCurve: s.Pool.String(),
//PairAddress: s.Pool.String(),
Maker: s.User.String(),
Token0Address: s.BaseMint.String(),
Token0Program: s.BaseTokenProgram.String(),
Token0Decimals: s.BaseMintDecimals,
Token1Address: "",
Token0Amount: s.BaseAmount.Div(decimal.NewFromInt(1e6)),
Token1Amount: s.QuoteAmount.Div(decimal.NewFromInt(1e9)),
Block: tx.Block,
BlockIndex: tx.BlockIndex,
Event: s.Event,
CachedTxHash: "",
txHash: tx.TxHash,
TxIndex: uint64(i),
Program: s.Program,
BlockAt: pgtype.Timestamptz{
Time: time.Unix(tx.BlockAt, 0),
},
//CreatedAt: nil,
TotalSupply: "1000000000",
AfterReserve0: s.BaseReserve.Div(decimal.New(1, int32(s.BaseMintDecimals))).String(),
AfterReserve1: s.QuoteReserve.Div(decimal.New(1, int32(s.QuoteMintDecimals))).String(),
QuoteIsToken0: false,
// CurrentPrice: decimal.Decimal{},
TokenCreator: s.Creator.String(),
Platform: platform,
PlatformFee: platformFee,
MevAgent: mev,
MevAgentFee: mevFee,
CUPrice: tx.CUPrice,
AfterSignerToken0Balance: s.UserBaseBalance.Div(decimal.New(1, int32(s.BaseMintDecimals))),
BeforeSolBalance: tx.BeforeSolBalance,
AfterSOLBalance: tx.AfterSOLBalance,
EntryContract: s.CheckEntryContract(),
Mayhem: s.Mayhem,
}
} else if s.Program == "PumpAMM" {
if s.BaseMint.Equals(solana.WrappedSol) {
eventName := s.Event
if s.Event == "buy" {
eventName = "sell"
} else if s.Event == "sell" {
eventName = "buy"
}
token0Program = s.QuoteTokenProgram
token0Address = s.QuoteMint
newTx = &Tx{
Err: nil,
//BondingCurve: s.Pool.String(),
PairAddress: s.Pool.String(),
Maker: s.User.String(),
Token1Address: s.BaseMint.String(),
Token0Program: s.QuoteTokenProgram.String(),
Token0Decimals: s.QuoteMintDecimals,
Token0Address: s.QuoteMint.String(),
Token1Amount: s.BaseAmount.Div(decimal.New(1, int32(s.BaseMintDecimals))),
Token0Amount: s.QuoteAmount.Div(decimal.New(1, int32(s.QuoteMintDecimals))),
Block: tx.Block,
BlockIndex: tx.BlockIndex,
Event: eventName,
CachedTxHash: "",
txHash: tx.TxHash,
TxIndex: uint64(i),
Program: s.Program,
BlockAt: pgtype.Timestamptz{
Time: time.Unix(tx.BlockAt, 0),
},
//CreatedAt: nil,
TotalSupply: "1000000000",
AfterReserve1: s.BaseReserve.Div(decimal.New(1, int32(s.BaseMintDecimals))).String(),
AfterReserve0: s.QuoteReserve.Div(decimal.New(1, int32(s.QuoteMintDecimals))).String(),
QuoteIsToken0: true,
// CurrentPrice: decimal.Decimal{},
TokenCreator: s.Creator.String(),
Platform: platform,
PlatformFee: platformFee,
MevAgent: mev,
MevAgentFee: mevFee,
CUPrice: tx.CUPrice,
AfterSignerToken0Balance: s.UserQuoteBalance.Div(decimal.New(1, int32(s.QuoteMintDecimals))),
BeforeSolBalance: tx.BeforeSolBalance,
AfterSOLBalance: tx.AfterSOLBalance,
EntryContract: s.CheckEntryContract(),
Mayhem: s.Mayhem,
}
} else {
newTx = &Tx{
Err: nil,
//BondingCurve: s.Pool.String(),
PairAddress: s.Pool.String(),
Maker: s.User.String(),
Token0Address: s.BaseMint.String(),
Token0Program: s.BaseTokenProgram.String(),
Token0Decimals: s.BaseMintDecimals,
Token1Address: s.QuoteMint.String(),
Token0Amount: s.BaseAmount.Div(decimal.New(1, int32(s.BaseMintDecimals))),
Token1Amount: s.QuoteAmount.Div(decimal.New(1, int32(s.QuoteMintDecimals))),
Block: tx.Block,
BlockIndex: tx.BlockIndex,
Event: s.Event,
CachedTxHash: "",
txHash: tx.TxHash,
TxIndex: uint64(i),
Program: s.Program,
BlockAt: pgtype.Timestamptz{
Time: time.Unix(tx.BlockAt, 0),
},
//CreatedAt: nil,
TotalSupply: "1000000000",
AfterReserve0: s.BaseReserve.Div(decimal.New(1, int32(s.BaseMintDecimals))).String(),
AfterReserve1: s.QuoteReserve.Div(decimal.New(1, int32(s.QuoteMintDecimals))).String(),
QuoteIsToken0: false,
// CurrentPrice: decimal.Decimal{},
TokenCreator: s.Creator.String(),
Platform: platform,
PlatformFee: platformFee,
MevAgent: mev,
MevAgentFee: mevFee,
CUPrice: tx.CUPrice,
AfterSignerToken0Balance: s.UserBaseBalance.Div(decimal.New(1, int32(s.BaseMintDecimals))),
BeforeSolBalance: tx.BeforeSolBalance,
AfterSOLBalance: tx.AfterSOLBalance,
EntryContract: s.CheckEntryContract(),
Mayhem: s.Mayhem,
}
}
}
if newTx == nil {
continue
}
if newTx.Maker == "HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K" && newTx.EntryContract == "oKXAggregatorV2" {
newTx.Maker = tx.Signer.String()
newTx.AfterSignerToken0Balance = parser.GetTokenBalanceAfterTx(raw, 0, token0Program, token0Address)
}
txs = append(txs, newTx)
}
return txs
}