node/pythnet: websocket subscription (#2219)
* node/pythnet: websocket subscription
This commit is contained in:
parent
e6d3bb8731
commit
3580f51ccf
18
Tiltfile
18
Tiltfile
|
@ -45,6 +45,7 @@ config.define_bool("aptos", False, "Enable Aptos component")
|
||||||
config.define_bool("algorand", False, "Enable Algorand component")
|
config.define_bool("algorand", False, "Enable Algorand component")
|
||||||
config.define_bool("evm2", False, "Enable second Eth component")
|
config.define_bool("evm2", False, "Enable second Eth component")
|
||||||
config.define_bool("solana", False, "Enable Solana component")
|
config.define_bool("solana", False, "Enable Solana component")
|
||||||
|
config.define_bool("pythnet", False, "Enable PythNet component")
|
||||||
config.define_bool("terra_classic", False, "Enable Terra Classic component")
|
config.define_bool("terra_classic", False, "Enable Terra Classic component")
|
||||||
config.define_bool("terra2", False, "Enable Terra 2 component")
|
config.define_bool("terra2", False, "Enable Terra 2 component")
|
||||||
config.define_bool("spy_relayer", False, "Enable spy relayer")
|
config.define_bool("spy_relayer", False, "Enable spy relayer")
|
||||||
|
@ -69,6 +70,7 @@ aptos = cfg.get("aptos", ci)
|
||||||
sui = cfg.get("sui", False)
|
sui = cfg.get("sui", False)
|
||||||
evm2 = cfg.get("evm2", ci)
|
evm2 = cfg.get("evm2", ci)
|
||||||
solana = cfg.get("solana", ci)
|
solana = cfg.get("solana", ci)
|
||||||
|
pythnet = cfg.get("pythnet", False)
|
||||||
terra_classic = cfg.get("terra_classic", ci)
|
terra_classic = cfg.get("terra_classic", ci)
|
||||||
terra2 = cfg.get("terra2", ci)
|
terra2 = cfg.get("terra2", ci)
|
||||||
wormchain = cfg.get("wormchain", ci)
|
wormchain = cfg.get("wormchain", ci)
|
||||||
|
@ -208,6 +210,18 @@ def build_node_yaml():
|
||||||
"http://solana-devnet:8899",
|
"http://solana-devnet:8899",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if pythnet:
|
||||||
|
container["command"] += [
|
||||||
|
"--pythnetRPC",
|
||||||
|
# "http://solana-devnet:8899",
|
||||||
|
"http://pythnet.rpcpool.com",
|
||||||
|
"--pythnetWS",
|
||||||
|
# "ws://solana-devnet:8900",
|
||||||
|
"wss://pythnet.rpcpool.com",
|
||||||
|
"--pythnetContract",
|
||||||
|
"H3fxXJ86ADW2PNuDDmZJg6mzTtPxkYCpNuQUTgmJ7AjU",
|
||||||
|
]
|
||||||
|
|
||||||
if terra_classic:
|
if terra_classic:
|
||||||
container["command"] += [
|
container["command"] += [
|
||||||
"--terraWS",
|
"--terraWS",
|
||||||
|
@ -270,7 +284,7 @@ k8s_yaml_with_ns(build_node_yaml())
|
||||||
guardian_resource_deps = ["eth-devnet"]
|
guardian_resource_deps = ["eth-devnet"]
|
||||||
if evm2:
|
if evm2:
|
||||||
guardian_resource_deps = guardian_resource_deps + ["eth-devnet2"]
|
guardian_resource_deps = guardian_resource_deps + ["eth-devnet2"]
|
||||||
if solana:
|
if solana or pythnet:
|
||||||
guardian_resource_deps = guardian_resource_deps + ["solana-devnet"]
|
guardian_resource_deps = guardian_resource_deps + ["solana-devnet"]
|
||||||
if near:
|
if near:
|
||||||
guardian_resource_deps = guardian_resource_deps + ["near"]
|
guardian_resource_deps = guardian_resource_deps + ["near"]
|
||||||
|
@ -366,7 +380,7 @@ k8s_resource(
|
||||||
trigger_mode = trigger_mode,
|
trigger_mode = trigger_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
if solana:
|
if solana or pythnet:
|
||||||
# solana client cli (used for devnet setup)
|
# solana client cli (used for devnet setup)
|
||||||
|
|
||||||
docker_build(
|
docker_build(
|
||||||
|
|
|
@ -165,6 +165,7 @@ var (
|
||||||
|
|
||||||
pythnetContract *string
|
pythnetContract *string
|
||||||
pythnetRPC *string
|
pythnetRPC *string
|
||||||
|
pythnetWS *string
|
||||||
|
|
||||||
arbitrumRPC *string
|
arbitrumRPC *string
|
||||||
arbitrumContract *string
|
arbitrumContract *string
|
||||||
|
@ -307,10 +308,11 @@ func init() {
|
||||||
suiAccount = NodeCmd.Flags().String("suiAccount", "", "sui account")
|
suiAccount = NodeCmd.Flags().String("suiAccount", "", "sui account")
|
||||||
suiPackage = NodeCmd.Flags().String("suiPackage", "", "sui package")
|
suiPackage = NodeCmd.Flags().String("suiPackage", "", "sui package")
|
||||||
|
|
||||||
solanaRPC = NodeCmd.Flags().String("solanaRPC", "", "Solana RPC URL (required")
|
solanaRPC = NodeCmd.Flags().String("solanaRPC", "", "Solana RPC URL (required)")
|
||||||
|
|
||||||
pythnetContract = NodeCmd.Flags().String("pythnetContract", "", "Address of the PythNet program (required)")
|
pythnetContract = NodeCmd.Flags().String("pythnetContract", "", "Address of the PythNet program (required)")
|
||||||
pythnetRPC = NodeCmd.Flags().String("pythnetRPC", "", "PythNet RPC URL (required")
|
pythnetRPC = NodeCmd.Flags().String("pythnetRPC", "", "PythNet RPC URL (required)")
|
||||||
|
pythnetWS = NodeCmd.Flags().String("pythnetWS", "", "PythNet WS URL")
|
||||||
|
|
||||||
arbitrumRPC = NodeCmd.Flags().String("arbitrumRPC", "", "Arbitrum RPC URL")
|
arbitrumRPC = NodeCmd.Flags().String("arbitrumRPC", "", "Arbitrum RPC URL")
|
||||||
arbitrumContract = NodeCmd.Flags().String("arbitrumContract", "", "Arbitrum contract address")
|
arbitrumContract = NodeCmd.Flags().String("arbitrumContract", "", "Arbitrum contract address")
|
||||||
|
@ -1280,11 +1282,11 @@ func runNode(cmd *cobra.Command, args []string) {
|
||||||
readiness.RegisterComponent(common.ReadinessSolanaSyncing)
|
readiness.RegisterComponent(common.ReadinessSolanaSyncing)
|
||||||
chainObsvReqC[vaa.ChainIDSolana] = make(chan *gossipv1.ObservationRequest, observationRequestBufferSize)
|
chainObsvReqC[vaa.ChainIDSolana] = make(chan *gossipv1.ObservationRequest, observationRequestBufferSize)
|
||||||
if err := supervisor.Run(ctx, "solwatch-confirmed",
|
if err := supervisor.Run(ctx, "solwatch-confirmed",
|
||||||
solana.NewSolanaWatcher(*solanaRPC, solAddress, lockC, nil, rpc.CommitmentConfirmed, common.ReadinessSolanaSyncing, vaa.ChainIDSolana).Run); err != nil {
|
common.WrapWithScissors(solana.NewSolanaWatcher(*solanaRPC, nil, solAddress, *solanaContract, lockC, nil, rpc.CommitmentConfirmed, common.ReadinessSolanaSyncing, vaa.ChainIDSolana).Run, "solwatch-confirmed")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
solanaFinalizedWatcher = solana.NewSolanaWatcher(*solanaRPC, solAddress, lockC, chainObsvReqC[vaa.ChainIDSolana], rpc.CommitmentFinalized, common.ReadinessSolanaSyncing, vaa.ChainIDSolana)
|
solanaFinalizedWatcher = solana.NewSolanaWatcher(*solanaRPC, nil, solAddress, *solanaContract, lockC, chainObsvReqC[vaa.ChainIDSolana], rpc.CommitmentFinalized, common.ReadinessSolanaSyncing, vaa.ChainIDSolana)
|
||||||
if err := supervisor.Run(ctx, "solwatch-finalized", solanaFinalizedWatcher.Run); err != nil {
|
if err := supervisor.Run(ctx, "solwatch-finalized", common.WrapWithScissors(solanaFinalizedWatcher.Run, "solwatch-finalized")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1294,11 +1296,7 @@ func runNode(cmd *cobra.Command, args []string) {
|
||||||
readiness.RegisterComponent(common.ReadinessPythNetSyncing)
|
readiness.RegisterComponent(common.ReadinessPythNetSyncing)
|
||||||
chainObsvReqC[vaa.ChainIDPythNet] = make(chan *gossipv1.ObservationRequest, observationRequestBufferSize)
|
chainObsvReqC[vaa.ChainIDPythNet] = make(chan *gossipv1.ObservationRequest, observationRequestBufferSize)
|
||||||
if err := supervisor.Run(ctx, "pythwatch-confirmed",
|
if err := supervisor.Run(ctx, "pythwatch-confirmed",
|
||||||
solana.NewSolanaWatcher(*pythnetRPC, pythnetAddress, lockC, nil, rpc.CommitmentConfirmed, common.ReadinessPythNetSyncing, vaa.ChainIDPythNet).Run); err != nil {
|
common.WrapWithScissors(solana.NewSolanaWatcher(*pythnetRPC, pythnetWS, pythnetAddress, *pythnetContract, lockC, nil, rpc.CommitmentConfirmed, common.ReadinessPythNetSyncing, vaa.ChainIDPythNet).Run, "pythwatch-confirmed")); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := supervisor.Run(ctx, "pythwatch-finalized",
|
|
||||||
solana.NewSolanaWatcher(*pythnetRPC, pythnetAddress, lockC, chainObsvReqC[vaa.ChainIDPythNet], rpc.CommitmentFinalized, common.ReadinessPythNetSyncing, vaa.ChainIDPythNet).Run); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/certusone/wormhole/node/pkg/common"
|
"github.com/certusone/wormhole/node/pkg/common"
|
||||||
"github.com/certusone/wormhole/node/pkg/p2p"
|
"github.com/certusone/wormhole/node/pkg/p2p"
|
||||||
gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
|
gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
|
||||||
|
@ -16,20 +20,27 @@ import (
|
||||||
"github.com/gagliardetto/solana-go"
|
"github.com/gagliardetto/solana-go"
|
||||||
"github.com/gagliardetto/solana-go/rpc"
|
"github.com/gagliardetto/solana-go/rpc"
|
||||||
"github.com/gagliardetto/solana-go/rpc/jsonrpc"
|
"github.com/gagliardetto/solana-go/rpc/jsonrpc"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/mr-tron/base58"
|
"github.com/mr-tron/base58"
|
||||||
"github.com/near/borsh-go"
|
"github.com/near/borsh-go"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"nhooyr.io/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SolanaWatcher struct {
|
type (
|
||||||
|
SolanaWatcher struct {
|
||||||
contract solana.PublicKey
|
contract solana.PublicKey
|
||||||
|
rawContract string
|
||||||
rpcUrl string
|
rpcUrl string
|
||||||
|
wsUrl *string
|
||||||
commitment rpc.CommitmentType
|
commitment rpc.CommitmentType
|
||||||
messageEvent chan *common.MessagePublication
|
messageEvent chan *common.MessagePublication
|
||||||
obsvReqC chan *gossipv1.ObservationRequest
|
obsvReqC chan *gossipv1.ObservationRequest
|
||||||
|
errC chan error
|
||||||
|
pumpData chan []byte
|
||||||
rpcClient *rpc.Client
|
rpcClient *rpc.Client
|
||||||
// Readiness component
|
// Readiness component
|
||||||
readiness readiness.Component
|
readiness readiness.Component
|
||||||
|
@ -39,12 +50,61 @@ type SolanaWatcher struct {
|
||||||
networkName string
|
networkName string
|
||||||
// The last slot processed by the watcher.
|
// The last slot processed by the watcher.
|
||||||
lastSlot uint64
|
lastSlot uint64
|
||||||
|
// subscriber id
|
||||||
|
subId string
|
||||||
|
|
||||||
// latestFinalizedBlockNumber is the latest block processed by this watcher.
|
// latestFinalizedBlockNumber is the latest block processed by this watcher.
|
||||||
latestBlockNumber uint64
|
latestBlockNumber uint64
|
||||||
latestBlockNumberMu sync.Mutex
|
latestBlockNumberMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EventSubscriptionError struct {
|
||||||
|
Jsonrpc string `json:"jsonrpc"`
|
||||||
|
Error struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message *string `json:"message"`
|
||||||
|
} `json:"error"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
EventSubscriptionData struct {
|
||||||
|
Jsonrpc string `json:"jsonrpc"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params *struct {
|
||||||
|
Result struct {
|
||||||
|
Context struct {
|
||||||
|
Slot int64 `json:"slot"`
|
||||||
|
} `json:"context"`
|
||||||
|
Value struct {
|
||||||
|
Pubkey string `json:"pubkey"`
|
||||||
|
Account struct {
|
||||||
|
Lamports int64 `json:"lamports"`
|
||||||
|
Data []string `json:"data"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Executable bool `json:"executable"`
|
||||||
|
RentEpoch int64 `json:"rentEpoch"`
|
||||||
|
} `json:"account"`
|
||||||
|
} `json:"value"`
|
||||||
|
} `json:"result"`
|
||||||
|
Subscription int `json:"subscription"`
|
||||||
|
} `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
MessagePublicationAccount struct {
|
||||||
|
VaaVersion uint8
|
||||||
|
// Borsh does not seem to support booleans, so 0=false / 1=true
|
||||||
|
ConsistencyLevel uint8
|
||||||
|
VaaTime uint32
|
||||||
|
VaaSignatureAccount vaa.Address
|
||||||
|
SubmissionTime uint32
|
||||||
|
Nonce uint32
|
||||||
|
Sequence uint64
|
||||||
|
EmitterChain uint16
|
||||||
|
EmitterAddress vaa.Address
|
||||||
|
Payload []byte
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
solanaConnectionErrors = promauto.NewCounterVec(
|
solanaConnectionErrors = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
|
@ -116,7 +176,9 @@ type PostMessageData struct {
|
||||||
|
|
||||||
func NewSolanaWatcher(
|
func NewSolanaWatcher(
|
||||||
rpcUrl string,
|
rpcUrl string,
|
||||||
|
wsUrl *string,
|
||||||
contractAddress solana.PublicKey,
|
contractAddress solana.PublicKey,
|
||||||
|
rawContract string,
|
||||||
messageEvents chan *common.MessagePublication,
|
messageEvents chan *common.MessagePublication,
|
||||||
obsvReqC chan *gossipv1.ObservationRequest,
|
obsvReqC chan *gossipv1.ObservationRequest,
|
||||||
commitment rpc.CommitmentType,
|
commitment rpc.CommitmentType,
|
||||||
|
@ -124,7 +186,9 @@ func NewSolanaWatcher(
|
||||||
chainID vaa.ChainID) *SolanaWatcher {
|
chainID vaa.ChainID) *SolanaWatcher {
|
||||||
return &SolanaWatcher{
|
return &SolanaWatcher{
|
||||||
rpcUrl: rpcUrl,
|
rpcUrl: rpcUrl,
|
||||||
|
wsUrl: wsUrl,
|
||||||
contract: contractAddress,
|
contract: contractAddress,
|
||||||
|
rawContract: rawContract,
|
||||||
messageEvent: messageEvents,
|
messageEvent: messageEvents,
|
||||||
obsvReqC: obsvReqC,
|
obsvReqC: obsvReqC,
|
||||||
commitment: commitment,
|
commitment: commitment,
|
||||||
|
@ -135,6 +199,81 @@ func NewSolanaWatcher(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SolanaWatcher) SetupSubscription(ctx context.Context) (error, *websocket.Conn) {
|
||||||
|
logger := supervisor.Logger(ctx)
|
||||||
|
|
||||||
|
logger.Info("Solana watcher connecting to WS node ", zap.String("url", *s.wsUrl))
|
||||||
|
|
||||||
|
ws, _, err := websocket.Dial(ctx, *s.wsUrl, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.subId = uuid.New().String()
|
||||||
|
|
||||||
|
s.pumpData = make(chan []byte)
|
||||||
|
|
||||||
|
const temp = `{"jsonrpc": "2.0", "id": "%s", "method": "programSubscribe", "params": ["%s", {"encoding": "base64", "commitment": "%s", "filters": []}]}`
|
||||||
|
var p = fmt.Sprintf(temp, s.subId, s.rawContract, string(s.commitment))
|
||||||
|
|
||||||
|
logger.Info("Subscribing using", zap.String("filter", p))
|
||||||
|
|
||||||
|
if err := ws.Write(ctx, websocket.MessageText, []byte(p)); err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("write: %s", err.Error()))
|
||||||
|
return err, nil
|
||||||
|
}
|
||||||
|
return nil, ws
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SolanaWatcher) SetupWebSocket(ctx context.Context) error {
|
||||||
|
if vaa.ChainID(s.chainID) != vaa.ChainIDPythNet {
|
||||||
|
panic("unsupported chain id")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := supervisor.Logger(ctx)
|
||||||
|
|
||||||
|
err, ws := s.SetupSubscription(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
common.RunWithScissors(ctx, s.errC, "SolanaDataPump", func(ctx context.Context) error {
|
||||||
|
defer ws.Close(websocket.StatusNormalClosure, "")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
rCtx, cancel := context.WithTimeout(ctx, time.Second*300) // 5 minute
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if _, msg, err := ws.Read(rCtx); err != nil {
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
// When a websocket context times out, it closes the websocket... This means we have to re-subscribe
|
||||||
|
ws.Close(websocket.StatusNormalClosure, "")
|
||||||
|
err, ws = s.SetupSubscription(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(fmt.Sprintf("ReadMessage: '%s'", err.Error()))
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.pumpData <- msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SolanaWatcher) Run(ctx context.Context) error {
|
func (s *SolanaWatcher) Run(ctx context.Context) error {
|
||||||
// Initialize gossip metrics (we want to broadcast the address even if we're not yet syncing)
|
// Initialize gossip metrics (we want to broadcast the address even if we're not yet syncing)
|
||||||
contractAddr := base58.Encode(s.contract[:])
|
contractAddr := base58.Encode(s.contract[:])
|
||||||
|
@ -143,16 +282,35 @@ func (s *SolanaWatcher) Run(ctx context.Context) error {
|
||||||
})
|
})
|
||||||
|
|
||||||
logger := supervisor.Logger(ctx)
|
logger := supervisor.Logger(ctx)
|
||||||
errC := make(chan error)
|
|
||||||
|
|
||||||
go func() {
|
logger.Info("Solana watcher connecting to RPC node ", zap.String("url", s.rpcUrl))
|
||||||
|
|
||||||
|
s.errC = make(chan error)
|
||||||
|
s.pumpData = make(chan []byte)
|
||||||
|
|
||||||
|
if s.wsUrl != nil {
|
||||||
|
err := s.SetupWebSocket(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
common.RunWithScissors(ctx, s.errC, "SolanaWatcher", func(ctx context.Context) error {
|
||||||
timer := time.NewTicker(time.Second * 1)
|
timer := time.NewTicker(time.Second * 1)
|
||||||
defer timer.Stop()
|
defer timer.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return nil
|
||||||
|
case msg := <-s.pumpData:
|
||||||
|
err := s.processAccountSubscriptionData(ctx, logger, msg)
|
||||||
|
if err != nil {
|
||||||
|
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
|
||||||
|
solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "account_subscription_data").Inc()
|
||||||
|
s.errC <- err
|
||||||
|
return err
|
||||||
|
}
|
||||||
case m := <-s.obsvReqC:
|
case m := <-s.obsvReqC:
|
||||||
if m.ChainId != uint32(s.chainID) {
|
if m.ChainId != uint32(s.chainID) {
|
||||||
panic("unexpected chain id")
|
panic("unexpected chain id")
|
||||||
|
@ -174,8 +332,8 @@ func (s *SolanaWatcher) Run(ctx context.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
|
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
|
||||||
solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "get_slot_error").Inc()
|
solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "get_slot_error").Inc()
|
||||||
errC <- err
|
s.errC <- err
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSlot := s.lastSlot
|
lastSlot := s.lastSlot
|
||||||
|
@ -189,6 +347,7 @@ func (s *SolanaWatcher) Run(ctx context.Context) error {
|
||||||
ContractAddress: contractAddr,
|
ContractAddress: contractAddr,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if s.wsUrl == nil {
|
||||||
rangeStart := lastSlot + 1
|
rangeStart := lastSlot + 1
|
||||||
rangeEnd := slot
|
rangeEnd := slot
|
||||||
|
|
||||||
|
@ -202,18 +361,23 @@ func (s *SolanaWatcher) Run(ctx context.Context) error {
|
||||||
|
|
||||||
// Requesting each slot
|
// Requesting each slot
|
||||||
for slot := rangeStart; slot <= rangeEnd; slot++ {
|
for slot := rangeStart; slot <= rangeEnd; slot++ {
|
||||||
go s.retryFetchBlock(ctx, logger, slot, 0)
|
_slot := slot
|
||||||
|
common.RunWithScissors(ctx, s.errC, "SolanaWatcherSlotFetcher", func(ctx context.Context) error {
|
||||||
|
s.retryFetchBlock(ctx, logger, _slot, 0)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.lastSlot = slot
|
s.lastSlot = slot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case err := <-errC:
|
case err := <-s.errC:
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,7 +401,10 @@ func (s *SolanaWatcher) retryFetchBlock(ctx context.Context, logger *zap.Logger,
|
||||||
zap.String("commitment", string(s.commitment)),
|
zap.String("commitment", string(s.commitment)),
|
||||||
zap.Uint("retry", retry))
|
zap.Uint("retry", retry))
|
||||||
|
|
||||||
go s.retryFetchBlock(ctx, logger, slot, retry+1)
|
common.RunWithScissors(ctx, s.errC, "retryFetchBlock", func(ctx context.Context) error {
|
||||||
|
s.retryFetchBlock(ctx, logger, slot, retry+1)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,10 +446,11 @@ func (s *SolanaWatcher) fetchBlock(ctx context.Context, logger *zap.Logger, slot
|
||||||
|
|
||||||
// Schedule a single retry just in case the Solana node was confused about the block being missing.
|
// Schedule a single retry just in case the Solana node was confused about the block being missing.
|
||||||
if emptyRetry < maxEmptyRetry {
|
if emptyRetry < maxEmptyRetry {
|
||||||
go func() {
|
common.RunWithScissors(ctx, s.errC, "delayedFetchBlock", func(ctx context.Context) error {
|
||||||
time.Sleep(retryDelay)
|
time.Sleep(retryDelay)
|
||||||
s.fetchBlock(ctx, logger, slot, emptyRetry+1)
|
s.fetchBlock(ctx, logger, slot, emptyRetry+1)
|
||||||
}()
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
|
@ -465,7 +633,10 @@ func (s *SolanaWatcher) processInstruction(ctx context.Context, logger *zap.Logg
|
||||||
logger.Debug("fetching VAA account", zap.Stringer("acc", acc),
|
logger.Debug("fetching VAA account", zap.Stringer("acc", acc),
|
||||||
zap.Stringer("signature", signature), zap.Uint64("slot", slot), zap.Int("idx", idx))
|
zap.Stringer("signature", signature), zap.Uint64("slot", slot), zap.Int("idx", idx))
|
||||||
|
|
||||||
go s.retryFetchMessageAccount(ctx, logger, acc, slot, 0)
|
common.RunWithScissors(ctx, s.errC, "retryFetchMessageAccount", func(ctx context.Context) error {
|
||||||
|
s.retryFetchMessageAccount(ctx, logger, acc, slot, 0)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
@ -491,7 +662,10 @@ func (s *SolanaWatcher) retryFetchMessageAccount(ctx context.Context, logger *za
|
||||||
zap.String("commitment", string(s.commitment)),
|
zap.String("commitment", string(s.commitment)),
|
||||||
zap.Uint("retry", retry))
|
zap.Uint("retry", retry))
|
||||||
|
|
||||||
go s.retryFetchMessageAccount(ctx, logger, acc, slot, retry+1)
|
common.RunWithScissors(ctx, s.errC, "retryFetchMessageAccount", func(ctx context.Context) error {
|
||||||
|
s.retryFetchMessageAccount(ctx, logger, acc, slot, retry+1)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -548,6 +722,64 @@ func (s *SolanaWatcher) fetchMessageAccount(ctx context.Context, logger *zap.Log
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SolanaWatcher) processAccountSubscriptionData(ctx context.Context, logger *zap.Logger, data []byte) error {
|
||||||
|
// Do we have an error on the subscription?
|
||||||
|
var e EventSubscriptionError
|
||||||
|
err := json.Unmarshal(data, &e)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(*s.wsUrl, zap.Error(err))
|
||||||
|
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Error.Message != nil {
|
||||||
|
return errors.New(*e.Error.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
var res EventSubscriptionData
|
||||||
|
err = json.Unmarshal(data, &res)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(*s.wsUrl, zap.Error(err))
|
||||||
|
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Params == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
value := (*res.Params).Result.Value
|
||||||
|
|
||||||
|
if value.Account.Owner != s.rawContract {
|
||||||
|
// We got a message for the wrong contract on the websocket... uncomfortable...
|
||||||
|
solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "invalid_websocket_account").Inc()
|
||||||
|
return errors.New("Update for account with wrong owner")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = base64.StdEncoding.DecodeString(value.Account.Data[0])
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(*s.wsUrl, zap.Error(err))
|
||||||
|
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore truncated messages
|
||||||
|
if len(data) < 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other accounts owned by the wormhole contract seem to send updates...
|
||||||
|
switch string(data[:3]) {
|
||||||
|
case accountPrefixReliable, accountPrefixUnreliable:
|
||||||
|
acc := solana.PublicKeyFromBytes([]byte(value.Pubkey))
|
||||||
|
s.processMessageAccount(logger, data, acc)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SolanaWatcher) processMessageAccount(logger *zap.Logger, data []byte, acc solana.PublicKey) {
|
func (s *SolanaWatcher) processMessageAccount(logger *zap.Logger, data []byte, acc solana.PublicKey) {
|
||||||
proposal, err := ParseMessagePublicationAccount(data)
|
proposal, err := ParseMessagePublicationAccount(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -618,22 +850,6 @@ func (s *SolanaWatcher) GetLatestFinalizedBlockNumber() uint64 {
|
||||||
return s.latestBlockNumber
|
return s.latestBlockNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
|
||||||
MessagePublicationAccount struct {
|
|
||||||
VaaVersion uint8
|
|
||||||
// Borsh does not seem to support booleans, so 0=false / 1=true
|
|
||||||
ConsistencyLevel uint8
|
|
||||||
VaaTime uint32
|
|
||||||
VaaSignatureAccount vaa.Address
|
|
||||||
SubmissionTime uint32
|
|
||||||
Nonce uint32
|
|
||||||
Sequence uint64
|
|
||||||
EmitterChain uint16
|
|
||||||
EmitterAddress vaa.Address
|
|
||||||
Payload []byte
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseMessagePublicationAccount(data []byte) (*MessagePublicationAccount, error) {
|
func ParseMessagePublicationAccount(data []byte) (*MessagePublicationAccount, error) {
|
||||||
prop := &MessagePublicationAccount{}
|
prop := &MessagePublicationAccount{}
|
||||||
// Skip the b"msg" prefix
|
// Skip the b"msg" prefix
|
||||||
|
|
Loading…
Reference in New Issue