diff --git a/chainlink.go b/chainlink.go new file mode 100644 index 0000000..5347507 --- /dev/null +++ b/chainlink.go @@ -0,0 +1,120 @@ +package pump_parser + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math/big" + + "github.com/gagliardetto/solana-go" + "github.com/shopspring/decimal" +) + +var ( + chainlinkSOLUSDFeedAccount = solana.MustPublicKeyFromBase58("CH31Xns5z3M1cTAbKW34jcxPPciazARpijcHj9rxtemt") + chainlinkSubmitDiscriminator = calculateDiscriminator("global:submit") +) + +func chainLinkParser(tx *Tx, instruction Instruction, inners InnerInstructions, offset [2]uint) ([2]uint, error) { + if !tx.rawTx.accountList[instruction.ProgramIDIndex].Equals(chainLinkProgram) { + return increaseOffset(offset), fmt.Errorf("system program instruction not found, block: %d, tx: %s, outerIndex: %d, innerIndex: %d", tx.rawTx.Slot, tx.rawTx.TxHash(), offset[0], offset[1]) + } + if tx.rawTx.Meta.Err != nil { + return increaseOffset(offset), nil + } + + decode := instruction.Data + discriminator := binary.LittleEndian.Uint32(decode[0:4]) + + switch discriminator { + case transferDiscriminator: + return chainLinkSubmitParser(instruction, inners, offset, tx, decode[4:]) + default: + return increaseOffset(offset), nil + } +} + +func chainLinkSubmitParser(instruction Instruction, inners InnerInstructions, offset [2]uint, tx *Tx, decodeData []byte) ([2]uint, error) { + if len(instruction.Accounts) < 6 { + return increaseOffset(offset), InstructionIgnoredError + } + + inner, err := getInnerInstructions(inners, offset[1]) + if err != nil { + return increaseOffset(offset), err + } + + if len(inner) < 1 { + return increaseOffset(offset), InstructionIgnoredError + } + storeInstruction := inner[0] + if len(storeInstruction.Accounts) < 2 { + return increaseOffset(offset), InstructionIgnoredError + } + if storeInstruction.Accounts[0] >= len(tx.rawTx.accountList) || tx.rawTx.accountList[storeInstruction.Accounts[0]] != chainlinkSOLUSDFeedAccount { + return increaseOffset(offset), InstructionIgnoredError + } + if !bytes.Equal(storeInstruction.Data[0:8], chainlinkSubmitDiscriminator[:]) { + return increaseOffset(offset), InstructionIgnoredError + } + data, err := parseChainLinkSubmitData(storeInstruction.Data) + if err != nil { + return increaseOffset(offset), InstructionIgnoredError + } + tx.ChainLink.Timestamp = int64(data.Timestamp) + tx.ChainLink.Price = decimal.NewFromBigInt(data.Price(), -8) + return increaseOffset(offset), nil +} + +type SubmitData struct { + Discriminator [8]byte + Timestamp uint64 + Answer [16]byte +} + +func parseChainLinkSubmitData(data []byte) (*SubmitData, error) { + if len(data) != 32 { + return nil, errors.New("invalid submit data length") + } + var submitData SubmitData + copy(submitData.Discriminator[:], data[:8]) + submitData.Timestamp = binary.LittleEndian.Uint64(data[8:16]) + copy(submitData.Answer[:], data[16:32]) + return &submitData, nil +} + +func (s *SubmitData) Price() *big.Int { + return int128LEBytesToBigInt(s.Answer) +} + +func int128LEBytesToBigInt(bytes [16]byte) *big.Int { + // Create new big.Int + bigInt := new(big.Int) + + // Reverse bytes for little-endian to big-endian conversion + reversed := make([]byte, 16) + for i := 0; i < 16; i++ { + reversed[15-i] = bytes[i] + } + + // Check if negative (first byte in little-endian is highest byte) + isNegative := bytes[15]&0x80 != 0 + + if isNegative { + // If negative, flip all bits + for i := range reversed { + reversed[i] = ^reversed[i] + } + // Convert to big.Int + bigInt.SetBytes(reversed) + // Add 1 and negate + bigInt.Add(bigInt, big.NewInt(1)) + bigInt.Neg(bigInt) + } else { + // If positive, convert directly + bigInt.SetBytes(reversed) + } + + return bigInt +} diff --git a/meta.go b/meta.go index 09cb4b9..a92cc59 100644 --- a/meta.go +++ b/meta.go @@ -236,4 +236,6 @@ var createAccountWithSeedDiscriminator = uint32(3) var systemProgram = solana.MustPublicKeyFromBase58("11111111111111111111111111111111") var momoProgram = solana.MustPublicKeyFromBase58("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr") +var chainLinkProgram = solana.MustPublicKeyFromBase58("cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ") + var eventDiscriminator = [8]byte{228, 69, 165, 46, 81, 203, 154, 29} diff --git a/parser.go b/parser.go index a5379f3..cceccfa 100644 --- a/parser.go +++ b/parser.go @@ -60,8 +60,9 @@ func WithMeteoraDlmm() ParserOption { } var actionPrograms = map[solana.PublicKey]actionParser{ - systemProgram: systemParser, - budgGetProgram: budgetParser, + systemProgram: systemParser, + budgGetProgram: budgetParser, + chainLinkProgram: chainLinkParser, } func ParseRawTx(rawTx *RawTx) (*Tx, error) { diff --git a/tx.go b/tx.go index 379b89f..d06537d 100644 --- a/tx.go +++ b/tx.go @@ -74,6 +74,10 @@ type SolTransfer struct { To solana.PublicKey Amount decimal.Decimal } +type ChainLink struct { + Timestamp int64 + Price decimal.Decimal +} type Tx struct { rawTx *RawTx @@ -83,6 +87,7 @@ type Tx struct { Swaps []Swap `json:"swaps,omitempty"` SolTransfer []SolTransfer `json:"sol_transfer,omitempty"` Block uint64 `json:"block"` + ChainLink ChainLink `json:"chain_link"` BlockIndex uint64 `json:"index"` TxHash *[64]byte `json:"-"` BlockAt int64 `json:"block_at"`