From 1af28d537790e72ef334193e5c93ca191c689ab1 Mon Sep 17 00:00:00 2001 From: blockchain-develop <43429291+blockchain-develop@users.noreply.github.com> Date: Fri, 11 Oct 2024 01:04:33 +0800 Subject: [PATCH] Subscribe block (#203) * update * udpate * update * updat * update * update * update --- rpc/getBlock.go | 78 ++++++++++++++++++++- rpc/getTransaction.go | 2 +- rpc/types.go | 13 ++-- rpc/ws/blockSubscribe.go | 2 +- rpc/ws/parsedBlockSubscribe.go | 123 +++++++++++++++++++++++++++++++++ transaction.go | 63 +++++++++++++++++ 6 files changed, 272 insertions(+), 9 deletions(-) create mode 100644 rpc/ws/parsedBlockSubscribe.go diff --git a/rpc/getBlock.go b/rpc/getBlock.go index 3c831f29..760656d3 100644 --- a/rpc/getBlock.go +++ b/rpc/getBlock.go @@ -104,7 +104,7 @@ func (cl *Client) GetBlockWithOpts( opts.Encoding, // Valid encodings: // solana.EncodingJSON, // TODO - // solana.EncodingJSONParsed, // TODO + solana.EncodingJSONParsed, // TODO solana.EncodingBase58, solana.EncodingBase64, solana.EncodingBase64Zstd, @@ -161,3 +161,79 @@ type GetBlockResult struct { // The number of blocks beneath this block. BlockHeight *uint64 `json:"blockHeight"` } + +func (cl *Client) GetParsedBlockWithOpts( + ctx context.Context, + slot uint64, + opts *GetBlockOpts, +) (out *GetParsedBlockResult, err error) { + + obj := M{ + "encoding": solana.EncodingJSONParsed, + } + + if opts != nil { + if opts.TransactionDetails != "" { + obj["transactionDetails"] = opts.TransactionDetails + } + if opts.Rewards != nil { + obj["rewards"] = opts.Rewards + } + if opts.Commitment != "" { + obj["commitment"] = opts.Commitment + } + if opts.MaxSupportedTransactionVersion != nil { + obj["maxSupportedTransactionVersion"] = *opts.MaxSupportedTransactionVersion + } + } + + params := []interface{}{slot, obj} + + err = cl.rpcClient.CallForInto(ctx, &out, "getBlock", params) + + if err != nil { + return nil, err + } + if out == nil { + // Block is not confirmed. + return nil, ErrNotConfirmed + } + return +} + +type GetParsedBlockResult struct { + // The blockhash of this block. + Blockhash solana.Hash `json:"blockhash"` + + // The blockhash of this block's parent; + // if the parent block is not available due to ledger cleanup, + // this field will return "11111111111111111111111111111111". + PreviousBlockhash solana.Hash `json:"previousBlockhash"` + + // The slot index of this block's parent. + ParentSlot uint64 `json:"parentSlot"` + + // Present if "full" transaction details are requested. + Transactions []ParsedTransactionWithMeta `json:"transactions"` + + // Present if "signatures" are requested for transaction details; + // an array of signatures, corresponding to the transaction order in the block. + Signatures []solana.Signature `json:"signatures"` + + // Present if rewards are requested. + Rewards []BlockReward `json:"rewards"` + + // Estimated production time, as Unix timestamp (seconds since the Unix epoch). + // Nil if not available. + BlockTime *solana.UnixTimeSeconds `json:"blockTime"` + + // The number of blocks beneath this block. + BlockHeight *uint64 `json:"blockHeight"` +} + +type ParsedTransactionWithMeta struct { + Slot uint64 + BlockTime *solana.UnixTimeSeconds + Transaction *ParsedTransaction + Meta *ParsedTransactionMeta +} diff --git a/rpc/getTransaction.go b/rpc/getTransaction.go index 85d0f64a..54f7522f 100644 --- a/rpc/getTransaction.go +++ b/rpc/getTransaction.go @@ -50,7 +50,7 @@ func (cl *Client) GetTransaction( opts.Encoding, // Valid encodings: // solana.EncodingJSON, // TODO - // solana.EncodingJSONParsed, // TODO + solana.EncodingJSONParsed, // TODO solana.EncodingBase58, solana.EncodingBase64, solana.EncodingBase64Zstd, diff --git a/rpc/types.go b/rpc/types.go index d601b21a..e36b93c9 100644 --- a/rpc/types.go +++ b/rpc/types.go @@ -215,7 +215,7 @@ type TransactionMeta struct { Rewards []BlockReward `json:"rewards"` LoadedAddresses LoadedAddresses `json:"loadedAddresses"` - + ComputeUnitsConsumed *uint64 `json:"computeUnitsConsumed"` } @@ -498,11 +498,12 @@ type ParsedMessage struct { } type ParsedInstruction struct { - Program string `json:"program,omitempty"` - ProgramId solana.PublicKey `json:"programId,omitempty"` - Parsed *InstructionInfoEnvelope `json:"parsed,omitempty"` - Data solana.Base58 `json:"data,omitempty"` - Accounts []solana.PublicKey `json:"accounts,omitempty"` + Program string `json:"program,omitempty"` + ProgramId solana.PublicKey `json:"programId,omitempty"` + Parsed *InstructionInfoEnvelope `json:"parsed,omitempty"` + Data solana.Base58 `json:"data,omitempty"` + Accounts []solana.PublicKey `json:"accounts,omitempty"` + StackHeight int `json:"stackHeight"` } type InstructionInfoEnvelope struct { diff --git a/rpc/ws/blockSubscribe.go b/rpc/ws/blockSubscribe.go index d392eb97..24e4bb57 100644 --- a/rpc/ws/blockSubscribe.go +++ b/rpc/ws/blockSubscribe.go @@ -103,7 +103,7 @@ func (cl *Client) BlockSubscribe( opts.Encoding, // Valid encodings: // solana.EncodingJSON, // TODO - // solana.EncodingJSONParsed, // TODO + solana.EncodingJSONParsed, // TODO solana.EncodingBase58, solana.EncodingBase64, solana.EncodingBase64Zstd, diff --git a/rpc/ws/parsedBlockSubscribe.go b/rpc/ws/parsedBlockSubscribe.go new file mode 100644 index 00000000..c6c7f58b --- /dev/null +++ b/rpc/ws/parsedBlockSubscribe.go @@ -0,0 +1,123 @@ +// Copyright 2022 github.com/gagliardetto +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ws + +import ( + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" +) + +type ParsedBlockResult struct { + Context struct { + Slot uint64 + } `json:"context"` + Value struct { + Slot uint64 `json:"slot"` + Err interface{} `json:"err,omitempty"` + Block *rpc.GetParsedBlockResult `json:"block,omitempty"` + } `json:"value"` +} + +// NOTE: Unstable, disabled by default +// +// Subscribe to receive notification anytime a new block is Confirmed or Finalized. +// +// **This subscription is unstable and only available if the validator was started +// with the `--rpc-pubsub-enable-block-subscription` flag. The format of this +// subscription may change in the future** +func (cl *Client) ParsedBlockSubscribe( + filter BlockSubscribeFilter, + opts *BlockSubscribeOpts, +) (*ParsedBlockSubscription, error) { + var params []interface{} + if filter != nil { + switch v := filter.(type) { + case BlockSubscribeFilterAll: + params = append(params, "all") + case *BlockSubscribeFilterMentionsAccountOrProgram: + params = append(params, rpc.M{"mentionsAccountOrProgram": v.Pubkey}) + } + } + if opts != nil { + obj := make(rpc.M) + if opts.Commitment != "" { + obj["commitment"] = opts.Commitment + } + obj["encoding"] = solana.EncodingJSONParsed + if opts.TransactionDetails != "" { + obj["transactionDetails"] = opts.TransactionDetails + } + if opts.Rewards != nil { + obj["rewards"] = opts.Rewards + } + if opts.MaxSupportedTransactionVersion != nil { + obj["maxSupportedTransactionVersion"] = *opts.MaxSupportedTransactionVersion + } + if len(obj) > 0 { + params = append(params, obj) + } + } + genSub, err := cl.subscribe( + params, + nil, + "blockSubscribe", + "blockUnsubscribe", + func(msg []byte) (interface{}, error) { + var res ParsedBlockResult + err := decodeResponseFromMessage(msg, &res) + return &res, err + }, + ) + if err != nil { + return nil, err + } + return &ParsedBlockSubscription{ + sub: genSub, + }, nil +} + +type ParsedBlockSubscription struct { + sub *Subscription +} + +func (sw *ParsedBlockSubscription) Recv() (*ParsedBlockResult, error) { + select { + case d := <-sw.sub.stream: + return d.(*ParsedBlockResult), nil + case err := <-sw.sub.err: + return nil, err + } +} + +func (sw *ParsedBlockSubscription) Err() <-chan error { + return sw.sub.err +} + +func (sw *ParsedBlockSubscription) Response() <-chan *ParsedBlockResult { + typedChan := make(chan *ParsedBlockResult, 1) + go func(ch chan *ParsedBlockResult) { + // TODO: will this subscription yield more than one result? + d, ok := <-sw.sub.stream + if !ok { + return + } + ch <- d.(*ParsedBlockResult) + }(typedChan) + return typedChan +} + +func (sw *ParsedBlockSubscription) Unsubscribe() { + sw.sub.Unsubscribe() +} diff --git a/transaction.go b/transaction.go index dfa05955..cab9bffa 100644 --- a/transaction.go +++ b/transaction.go @@ -119,6 +119,11 @@ func MustTransactionFromDecoder(decoder *bin.Decoder) *Transaction { return out } +const ( + AccountsTypeIndex = "Fee" + AccountsTypeKey = "Key" +) + type CompiledInstruction struct { // Index into the message.accountKeys array indicating the program account that executes this instruction. // NOTE: it is actually a uint8, but using a uint16 because uint8 is treated as a byte everywhere, @@ -128,10 +133,68 @@ type CompiledInstruction struct { // List of ordered indices into the message.accountKeys array indicating which accounts to pass to the program. // NOTE: it is actually a []uint8, but using a uint16 because []uint8 is treated as a []byte everywhere, // and that can be an issue. + AccountsWithKey []PublicKey `json:"omitempty"` + // Accounts []uint16 `json:"accounts"` // The program input data encoded in a base-58 string. Data Base58 `json:"data"` + + // + StackHeight int `json:"stackHeight"` +} + +type compiledInstruction struct { + // Index into the message.accountKeys array indicating the program account that executes this instruction. + // NOTE: it is actually a uint8, but using a uint16 because uint8 is treated as a byte everywhere, + // and that can be an issue. + ProgramIDIndex uint16 `json:"programIdIndex"` + + // List of ordered indices into the message.accountKeys array indicating which accounts to pass to the program. + // NOTE: it is actually a []uint8, but using a uint16 because []uint8 is treated as a []byte everywhere, + // and that can be an issue. + Accounts []interface{} `json:"accounts"` + + // The program input data encoded in a base-58 string. + Data Base58 `json:"data"` + + // + StackHeight int `json:"stackHeight"` +} + +func (ci *CompiledInstruction) MarshalJSON() ([]byte, error) { + return json.Marshal(ci) +} + +func (ci *CompiledInstruction) UnmarshalJSON(data []byte) error { + in := compiledInstruction{} + err := json.Unmarshal(data, &in) + if err != nil { + return err + } + // + ci.ProgramIDIndex = in.ProgramIDIndex + ci.Data = in.Data + ci.StackHeight = in.StackHeight + ci.Accounts = make([]uint16, 0) + if len(in.Accounts) == 0 { + return nil + } + _, ok := in.Accounts[0].(string) + if ok { + accountsWithKey := make([]PublicKey, len(in.Accounts)) + for i, item := range in.Accounts { + accountsWithKey[i] = MustPublicKeyFromBase58(item.(string)) + } + ci.AccountsWithKey = accountsWithKey + } else { + AccountsWithIndex := make([]uint16, len(in.Accounts)) + for i, item := range in.Accounts { + AccountsWithIndex[i] = uint16(item.(float64)) + } + ci.Accounts = AccountsWithIndex + } + return nil } func (ci *CompiledInstruction) ResolveInstructionAccounts(message *Message) ([]*AccountMeta, error) {