Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: push oracle relayer #39

Merged
merged 4 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions relayer/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
BUILD_DIR ?= $(CURDIR)/build
COMMIT := $(shell git log -1 --format='%H')

all: test-unit install

.PHONY: all

###############################################################################
## Version ##
###############################################################################

ifeq (,$(VERSION))
VERSION := $(shell git describe --exact-match 2>/dev/null)
# if VERSION is empty, then populate it with branch's name and raw commit hash
ifeq (,$(VERSION))
VERSION := $(BRANCH)-$(COMMIT)
endif
endif

###############################################################################
## Build / Install ##
###############################################################################

ldflags = -X github.com/ojo-network/ojo-evm/relayer/cmd.Version=$(VERSION) \
-X github.com/ojo-network/ojo-evm/relayer/cmd.Commit=$(COMMIT)

ifeq ($(LINK_STATICALLY),true)
ldflags += -linkmode=external -extldflags "-Wl,-z,muldefs -static"
endif

build_tags += $(BUILD_TAGS)

BUILD_FLAGS := -tags "$(build_tags)" -ldflags '$(ldflags)'

build: go.sum
@echo "--> Building..."
go build -mod=readonly -o $(BUILD_DIR)/ $(BUILD_FLAGS) ./...

install: go.sum
@echo "--> Installing..."
go install -mod=readonly $(BUILD_FLAGS) ./...

.PHONY: build install

###############################################################################
## Tests & Linting ##
###############################################################################

test-unit:
@echo "--> Running tests"
@go test -short -mod=readonly -race ./... -v

.PHONY: test-unit

test-integration:
@echo "--> Running Integration Tests"
@go test -mod=readonly ./tests/integration/... -v

.PHONY: test-integration

lint:
@echo "--> Running linter"
@go run github.com/golangci/golangci-lint/cmd/golangci-lint run --fix --timeout=8m

.PHONY: lint
189 changes: 189 additions & 0 deletions relayer/cmd/relayer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package cmd

import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/signal"
"strings"
"syscall"
"time"

"github.com/cosmos/cosmos-sdk/client/input"
"github.com/ojo-network/ojo-evm/relayer/config"
"github.com/ojo-network/ojo-evm/relayer/relayer"
"github.com/ojo-network/ojo-evm/relayer/relayer/client"
"github.com/ojo-network/ojo/app/params"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)

const (
logLevelJSON = "json"
logLevelText = "text"

flagLogLevel = "log-level"
flagLogFormat = "log-format"

envVariablePass = "KEYRING_PASS"
)

var rootCmd = &cobra.Command{
Use: "relayer [config-file]",
Args: cobra.ExactArgs(1),
Short: "relayer is a tool that interacts with the GMP module to relay price feeds to EVM.",
Long: `A tool that anyone can use to trigger relays from the Ojo blockchain to a given EVM.
This is used to support Ojo's "Core" price feeds, which receive periodic push updates in addition
to the pull-based relayer events triggered by the two-way GMP calls.`,
RunE: relayerCmdHandler,
}

func init() {
// We need to set our bech32 address prefix because it was moved
// out of ojo's init function.
// Ref: https://github.com/ojo-network/ojo/pull/63
params.SetAddressPrefixes()
rootCmd.PersistentFlags().String(flagLogLevel, zerolog.InfoLevel.String(), "logging level")
rootCmd.PersistentFlags().String(flagLogFormat, logLevelText, "logging format; must be either json or text")
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func relayerCmdHandler(cmd *cobra.Command, args []string) error {
logLvlStr, err := cmd.Flags().GetString(flagLogLevel)
if err != nil {
return err
}

logLvl, err := zerolog.ParseLevel(logLvlStr)
if err != nil {
return err
}

logFormatStr, err := cmd.Flags().GetString(flagLogFormat)
if err != nil {
return err
}

var logWriter io.Writer
switch strings.ToLower(logFormatStr) {
case logLevelJSON:
logWriter = os.Stderr

case logLevelText:
logWriter = zerolog.ConsoleWriter{Out: os.Stderr}

default:
return fmt.Errorf("invalid logging format: %s", logFormatStr)
}

cfg, err := config.LoadConfigFromFlags(args[0], "")
if err != nil {
return err
}

ctx, cancel := context.WithCancel(cmd.Context())
g, ctx := errgroup.WithContext(ctx)

logger := zerolog.New(logWriter).Level(logLvl).With().Timestamp().Logger()

// listen for and trap any OS signal to gracefully shutdown and exit
trapSignal(cancel, logger)

// Gather pass via env variable || std input
keyringPass, err := getKeyringPassword()
if err != nil {
return err
}

// placeholder rpc timeout
rpcTimeout := time.Second * 10

relayerClient, err := client.NewRelayerClient(
ctx,
logger,
cfg.Account.ChainID,
cfg.Keyring.Backend,
cfg.Keyring.Dir,
keyringPass,
cfg.RPC.TMRPCEndpoint,
rpcTimeout,
cfg.Account.Address,
cfg.RPC.GRPCEndpoint,
cfg.Gas,
)
if err != nil {
return err
}

relayer, err := relayer.New(logger, relayerClient, cfg)
if err != nil {
return err
}

g.Go(func() error {
// start the process that observes and publishes exchange prices
return startRelayer(ctx, logger, relayer)
})

// Block main process until all spawned goroutines have gracefully exited and
// signal has been captured in the main process or if an error occurs.
return g.Wait()
}

// trapSignal will listen for any OS signal and invoke Done on the main
// WaitGroup allowing the main process to gracefully exit.
func trapSignal(cancel context.CancelFunc, logger zerolog.Logger) {
sigCh := make(chan os.Signal, 1)

signal.Notify(sigCh, syscall.SIGTERM)
signal.Notify(sigCh, syscall.SIGINT)

go func() {
sig := <-sigCh
logger.Info().Str("signal", sig.String()).Msg("caught signal; shutting down...")
cancel()
}()
}

func getKeyringPassword() (string, error) {
reader := bufio.NewReader(os.Stdin)

pass := os.Getenv(envVariablePass)
if pass == "" {
return input.GetString("Enter keyring password", reader)
}
return pass, nil
}

func startRelayer(ctx context.Context, logger zerolog.Logger, r *relayer.Relayer) error {
srvErrCh := make(chan error, 1)

go func() {
logger.Info().Msg("starting relayer..")
srvErrCh <- r.Start(ctx)
}()

for {
select {
case <-ctx.Done():
logger.Info().Msg("shutting down relayer..")
return nil

case err := <-srvErrCh:
logger.Err(err).Msg("error starting the relayer relayer")
r.Stop()
return err
}
}
}
73 changes: 73 additions & 0 deletions relayer/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package config

import (
"errors"
"time"

"github.com/go-playground/validator/v10"
)

const (
SampleNodeConfigPath = "relayer.toml"
)

var (
validate = validator.New()

// ErrEmptyConfigPath defines a sentinel error for an empty config path.
ErrEmptyConfigPath = errors.New("empty configuration file path")
)

type (
// Config defines all necessary configuration parameters.
Config struct {
ConfigDir string `mapstructure:"config_dir"`
Account Account `mapstructure:"account" validate:"required,gt=0,dive,required"`
Keyring Keyring `mapstructure:"keyring" validate:"required,gt=0,dive,required"`
RPC RPC `mapstructure:"rpc" validate:"required,gt=0,dive,required"`
Gas uint64 `mapstructure:"gas"`
Relayer Relayer `mapstructure:"relayer" validate:"required,gt=0,dive,required"`
Assets []Assets `mapstructure:"assets" validate:"required,gt=0,dive,required"`
}

// Account defines account related configuration that is related to the Ojo
// network and transaction signing functionality.
Account struct {
ChainID string `mapstructure:"chain_id" validate:"required"`
Address string `mapstructure:"address" validate:"required"`
}

// Keyring defines the required Ojo keyring configuration.
Keyring struct {
Backend string `mapstructure:"backend" validate:"required"`
Dir string `mapstructure:"dir" validate:"required"`
}

// RPC defines RPC configuration of both the Ojo gRPC and Tendermint nodes.
RPC struct {
TMRPCEndpoint string `mapstructure:"tmrpc_endpoint" validate:"required"`
GRPCEndpoint string `mapstructure:"grpc_endpoint" validate:"required"`
RPCTimeout string `mapstructure:"rpc_timeout" validate:"required"`
}

Relayer struct {
Interval time.Duration `mapstructure:"interval" validate:"required"`
Deviation float64 `mapstructure:"deviation" validate:"required"`
Destination string `mapstructure:"destination" validate:"required"`
Contract string `mapstructure:"contract" validate:"required"`
Tokens string `mapstructure:"tokens" validate:"required"`
}

Assets struct {
Denom string `mapstructure:"denom" validate:"required"`
}
)

// Validate returns an error if the Config object is invalid.
func (c Config) Validate() (err error) {
return validate.Struct(c)
}

// noop
func (c *Config) setDefaults() {
}
Loading
Loading