From 4f438ae1884a40e873ff64a1656800f567cd0567 Mon Sep 17 00:00:00 2001 From: ftocal <46001274+ftocal@users.noreply.github.com> Date: Tue, 18 Apr 2023 12:09:31 -0300 Subject: [PATCH] Add notional asset lookup (#239) * Add notional job * fix coingecko integration * Add deployment for notional jobs --------- Co-authored-by: Agustin Pazos --- deploy/jobs/env/production.env | 13 ++ deploy/jobs/env/staging.env | 13 ++ deploy/jobs/env/test.env | 13 ++ deploy/jobs/notional.yaml | 34 +++++ go.work | 1 + jobs/Dockerfile | 21 +++ jobs/Makefile | 17 +++ jobs/README.md | 2 + jobs/cmd/main.go | 67 +++++++++ jobs/config/config.go | 33 +++++ jobs/go.mod | 26 ++++ jobs/go.sum | 118 +++++++++++++++ jobs/internal/coingecko/coingecko.go | 89 +++++++++++ jobs/jobs/jobs.go | 12 ++ jobs/jobs/notional/notional.go | 213 +++++++++++++++++++++++++++ 15 files changed, 672 insertions(+) create mode 100644 deploy/jobs/env/production.env create mode 100644 deploy/jobs/env/staging.env create mode 100644 deploy/jobs/env/test.env create mode 100644 deploy/jobs/notional.yaml create mode 100644 jobs/Dockerfile create mode 100644 jobs/Makefile create mode 100644 jobs/README.md create mode 100644 jobs/cmd/main.go create mode 100644 jobs/config/config.go create mode 100644 jobs/go.mod create mode 100644 jobs/go.sum create mode 100644 jobs/internal/coingecko/coingecko.go create mode 100644 jobs/jobs/jobs.go create mode 100644 jobs/jobs/notional/notional.go diff --git a/deploy/jobs/env/production.env b/deploy/jobs/env/production.env new file mode 100644 index 00000000..458fc95c --- /dev/null +++ b/deploy/jobs/env/production.env @@ -0,0 +1,13 @@ +ENVIRONMENT=production +NAMESPACE=wormscan +NAME=wormscan-notional-job +IMAGE_NAME= +RESOURCES_LIMITS_MEMORY=30Mi +RESOURCES_LIMITS_CPU=20m +RESOURCES_REQUESTS_MEMORY=15Mi +RESOURCES_REQUESTS_CPU=10m +P2P_NETWORK=mainnet +COINGECKO_URL=https://api.coingecko.com/api/v3 +NOTIONAL_CHANNEL=WORMSCAN:NOTIONAL +LOG_LEVEL=INFO +CRONTAB_SCHEDULE=*/5 * * * * diff --git a/deploy/jobs/env/staging.env b/deploy/jobs/env/staging.env new file mode 100644 index 00000000..b16392c4 --- /dev/null +++ b/deploy/jobs/env/staging.env @@ -0,0 +1,13 @@ +ENVIRONMENT=staging +NAMESPACE=wormscan +NAME=wormscan-notional-job +IMAGE_NAME= +RESOURCES_LIMITS_MEMORY=30Mi +RESOURCES_LIMITS_CPU=20m +RESOURCES_REQUESTS_MEMORY=15Mi +RESOURCES_REQUESTS_CPU=10m +P2P_NETWORK=mainnet +COINGECKO_URL=https://api.coingecko.com/api/v3 +NOTIONAL_CHANNEL=WORMSCAN:NOTIONAL +LOG_LEVEL=INFO +CRONTAB_SCHEDULE=*/5 * * * * diff --git a/deploy/jobs/env/test.env b/deploy/jobs/env/test.env new file mode 100644 index 00000000..be3e648d --- /dev/null +++ b/deploy/jobs/env/test.env @@ -0,0 +1,13 @@ +ENVIRONMENT=test +NAMESPACE=wormscan-testnet +NAME=wormscan-notional-job +IMAGE_NAME= +RESOURCES_LIMITS_MEMORY=30Mi +RESOURCES_LIMITS_CPU=20m +RESOURCES_REQUESTS_MEMORY=15Mi +RESOURCES_REQUESTS_CPU=10m +P2P_NETWORK=mainnet +COINGECKO_URL=https://api.coingecko.com/api/v3 +NOTIONAL_CHANNEL=WORMSCAN:NOTIONAL +LOG_LEVEL=INFO +CRONTAB_SCHEDULE=*/5 * * * * diff --git a/deploy/jobs/notional.yaml b/deploy/jobs/notional.yaml new file mode 100644 index 00000000..97e02f09 --- /dev/null +++ b/deploy/jobs/notional.yaml @@ -0,0 +1,34 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: notional + namespace: {{ .NAMESPACE }} +spec: + schedule: "{{ .CRONTAB_SCHEDULE }}" + jobTemplate: + spec: + template: + spec: + containers: + - name: {{ .NAME }} + image: {{ .IMAGE_NAME }} + imagePullPolicy: Always + env: + - name: ENV + value: {{ .ENVIRONMENT }} + - name: P2P_NETWORK + value: {{ .P2P_NETWORK }} + - name: LOG_LEVEL + value: {{ .LOG_LEVEL }} + - name: JOB_ID + value: JOB_NOTIONAL_USD + - name: COINGECKO_URL + value: {{ .COINGECKO_URL }} + - name: NOTIONAL_CHANNEL + value: {{ .NOTIONAL_CHANNEL }} + - name: CACHE_URL + valueFrom: + configMapKeyRef: + name: config + key: redis-uri + restartPolicy: OnFailure diff --git a/go.work b/go.work index dd2aa37d..98d3d634 100644 --- a/go.work +++ b/go.work @@ -10,4 +10,5 @@ use ( ./pipeline ./spy ./tx-tracker + ./jobs ) diff --git a/jobs/Dockerfile b/jobs/Dockerfile new file mode 100644 index 00000000..db2ee149 --- /dev/null +++ b/jobs/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker.io/docker/dockerfile:1.3@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2 +FROM --platform=linux/amd64 docker.io/golang:1.19.2@sha256:0467d7d12d170ed8d998a2dae4a09aa13d0aa56e6d23c4ec2b1e4faacf86a813 AS build + +WORKDIR /app + +COPY jobs jobs +COPY common common + +# Build the Go app +RUN cd jobs && CGO_ENABLED=0 GOOS=linux go build -o "./jobs-app" cmd/main.go + +############################ +# STEP 2 build a small image +############################ +FROM alpine +#Copy certificates +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +# Copy our static executable. +COPY --from=build "/app/jobs/jobs-app" "/jobs-app" +# Run the binary. +ENTRYPOINT ["/jobs-app"] diff --git a/jobs/Makefile b/jobs/Makefile new file mode 100644 index 00000000..4f9b4ae6 --- /dev/null +++ b/jobs/Makefile @@ -0,0 +1,17 @@ +SHELL := /bin/bash + + +## help: print this help message +.PHONY: help +help: + @echo 'Usage:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +build: + go build -o jobs cmd/main.go + +test: + go test -v -cover ./... + + +.PHONY: build doc test diff --git a/jobs/README.md b/jobs/README.md new file mode 100644 index 00000000..d2654190 --- /dev/null +++ b/jobs/README.md @@ -0,0 +1,2 @@ +# Jobs +This component contains the jobs to be scheduler. \ No newline at end of file diff --git a/jobs/cmd/main.go b/jobs/cmd/main.go new file mode 100644 index 00000000..1dd708b7 --- /dev/null +++ b/jobs/cmd/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "context" + "log" + "os" + + "github.com/go-redis/redis" + "github.com/wormhole-foundation/wormhole-explorer/common/logger" + "github.com/wormhole-foundation/wormhole-explorer/jobs/config" + "github.com/wormhole-foundation/wormhole-explorer/jobs/internal/coingecko" + "github.com/wormhole-foundation/wormhole-explorer/jobs/jobs" + "github.com/wormhole-foundation/wormhole-explorer/jobs/jobs/notional" + "go.uber.org/zap" +) + +type exitCode int + +func main() { + defer handleExit() + context := context.Background() + + // get the config + cfg, errConf := config.New(context) + if errConf != nil { + log.Fatal("error creating config", errConf) + } + + logger := logger.New("wormhole-explorer-jobs", logger.WithLevel(cfg.LogLevel)) + logger.Info("started job execution", zap.String("job_id", cfg.JobID)) + + var err error + switch cfg.JobID { + case jobs.JobIDNotional: + notionalJob := initNotionalJob(context, cfg, logger) + err = notionalJob.Run() + default: + logger.Fatal("Invalid job id", zap.String("job_id", cfg.JobID)) + } + + if err != nil { + logger.Error("failed job execution", zap.String("job_id", cfg.JobID), zap.Error(err)) + } else { + logger.Info("finish job execution successfully", zap.String("job_id", cfg.JobID)) + } + +} + +// initNotionalJob initializes notional job. +func initNotionalJob(ctx context.Context, cfg *config.Configuration, logger *zap.Logger) *notional.NotionalJob { + // init coingecko api client. + api := coingecko.NewCoingeckoAPI(cfg.CoingeckoURL) + // init redis client. + redisClient := redis.NewClient(&redis.Options{Addr: cfg.CacheURL}) + // create notional job. + notionalJob := notional.NewNotionalJob(api, redisClient, cfg.P2pNetwork, cfg.NotionalChannel, logger) + return notionalJob +} + +func handleExit() { + if r := recover(); r != nil { + if e, ok := r.(exitCode); ok { + os.Exit(int(e)) + } + panic(r) // not an Exit, bubble up + } +} diff --git a/jobs/config/config.go b/jobs/config/config.go new file mode 100644 index 00000000..179c9a8a --- /dev/null +++ b/jobs/config/config.go @@ -0,0 +1,33 @@ +// Package config implement a simple configuration package. +// It define a type [Configuration] that represent the aplication configuration +package config + +import ( + "context" + + "github.com/joho/godotenv" + "github.com/sethvargo/go-envconfig" +) + +// Configuration is the configuration for the job +type Configuration struct { + Env string `env:"ENV,default=development"` + LogLevel string `env:"LOG_LEVEL,default=INFO"` + JobID string `env:"JOB_ID,required"` + CoingeckoURL string `env:"COINGECKO_URL,required"` + CacheURL string `env:"CACHE_URL,required"` + NotionalChannel string `env:"NOTIONAL_CHANNEL,required"` + P2pNetwork string `env:"P2P_NETWORK,required"` +} + +// New creates a configuration with the values from .env file and environment variables. +func New(ctx context.Context) (*Configuration, error) { + _ = godotenv.Load(".env", "../.env") + + var configuration Configuration + if err := envconfig.Process(ctx, &configuration); err != nil { + return nil, err + } + + return &configuration, nil +} diff --git a/jobs/go.mod b/jobs/go.mod new file mode 100644 index 00000000..414ae501 --- /dev/null +++ b/jobs/go.mod @@ -0,0 +1,26 @@ +module github.com/wormhole-foundation/wormhole-explorer/jobs + +go 1.19 + +require ( + github.com/go-redis/redis v6.15.9+incompatible + github.com/joho/godotenv v1.5.1 + github.com/sethvargo/go-envconfig v0.9.0 + github.com/wormhole-foundation/wormhole-explorer/common v0.0.0-20230417134228-3c597917f5c8 + github.com/wormhole-foundation/wormhole/sdk v0.0.0-20230417145436-53703d8ffcf0 + go.uber.org/zap v1.24.0 +) + +require ( + github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/ethereum/go-ethereum v1.10.21 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/holiman/uint256 v1.2.1 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect + github.com/onsi/gomega v1.27.6 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/sys v0.6.0 // indirect +) diff --git a/jobs/go.sum b/jobs/go.sum new file mode 100644 index 00000000..f10ea9d2 --- /dev/null +++ b/jobs/go.sum @@ -0,0 +1,118 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= +github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/ethereum/go-ethereum v1.10.21 h1:5lqsEx92ZaZzRyOqBEXux4/UR06m296RGzN3ol3teJY= +github.com/ethereum/go-ethereum v1.10.21/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/holiman/uint256 v1.2.1 h1:XRtyuda/zw2l+Bq/38n5XUoEF72aSOu/77Thd9pPp2o= +github.com/holiman/uint256 v1.2.1/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sethvargo/go-envconfig v0.9.0 h1:Q6FQ6hVEeTECULvkJZakq3dZMeBQ3JUpcKMfPQbKMDE= +github.com/sethvargo/go-envconfig v0.9.0/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/wormhole-foundation/wormhole-explorer/common v0.0.0-20230417134228-3c597917f5c8 h1:nJPjdHphY0JGPorg3GrGzIf8J4YR1eyTalxT7MzPIZg= +github.com/wormhole-foundation/wormhole-explorer/common v0.0.0-20230417134228-3c597917f5c8/go.mod h1:wySbOH0GO2dRhkTktCCCBnZ4FgNIpy3fL4hEbNMz5KI= +github.com/wormhole-foundation/wormhole/sdk v0.0.0-20230417145436-53703d8ffcf0 h1:uEJOLDlkpDxpShkCbFobYPd3MHZpNpkpt0+iyQnb9x4= +github.com/wormhole-foundation/wormhole/sdk v0.0.0-20230417145436-53703d8ffcf0/go.mod h1:dE12DOucCq23gjGGGhtbyx41FBxuHxjpPvG+ArO+8t0= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/jobs/internal/coingecko/coingecko.go b/jobs/internal/coingecko/coingecko.go new file mode 100644 index 00000000..955a0045 --- /dev/null +++ b/jobs/internal/coingecko/coingecko.go @@ -0,0 +1,89 @@ +package coingecko + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/wormhole-foundation/wormhole-explorer/common/domain" +) + +// CoingeckoAPI is a client for the coingecko API +type CoingeckoAPI struct { + url string + client *http.Client +} + +// NewCoingeckoAPI creates a new coingecko client +func NewCoingeckoAPI(url string) *CoingeckoAPI { + return &CoingeckoAPI{ + url: url, + client: http.DefaultClient, + } +} + +// NotionalUSD is the response from the coingecko API. +type NotionalUSD struct { + Price *float64 `json:"usd"` +} + +// GetNotionalUSD returns the notional USD value for the given ids +// ids is a list of coingecko chain identifier. +func (c *CoingeckoAPI) GetNotionalUSD(ids []string) (map[string]NotionalUSD, error) { + var response map[string]NotionalUSD + notionalUrl := fmt.Sprintf("%s/simple/price?ids=%s&vs_currencies=usd", c.url, strings.Join(ids, ",")) + + req, err := http.NewRequest(http.MethodGet, notionalUrl, nil) + if err != nil { + return response, err + } + res, err := c.client.Do(req) + if err != nil { + return response, err + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return response, err + } + err = json.Unmarshal(body, &response) + return response, err +} + +// GetChainIDs returns the coingecko chain ids for the given p2p network. +func GetChainIDs(p2pNetwork string) []string { + if p2pNetwork == domain.P2pMainNet { + return []string{ + "solana", + "ethereum", + "terra-luna", + "binancecoin", + "matic-network", + "avalanche-2", + "oasis-network", + "algorand", + "aurora", + "fantom", + "karura", + "acala", + "klay-token", + "celo", + "near", + "moonbeam", + "neon", + "terra-luna-2", + "injective-protocol", + "aptos", + "sui", + "arbitrum", + "optimism", + "xpla", + "bitcoin", + "base-protocol"} + } + // TODO: define chains ids for testnet. + return []string{} +} diff --git a/jobs/jobs/jobs.go b/jobs/jobs/jobs.go new file mode 100644 index 00000000..0283833b --- /dev/null +++ b/jobs/jobs/jobs.go @@ -0,0 +1,12 @@ +// Package jobs define an interface to execute jobs +package jobs + +// JobIDNotional is the job id for notional job. +const ( + JobIDNotional = "JOB_NOTIONAL_USD" +) + +// Job is the interface for jobs. +type Job interface { + Run() error +} diff --git a/jobs/jobs/notional/notional.go b/jobs/jobs/notional/notional.go new file mode 100644 index 00000000..1304915d --- /dev/null +++ b/jobs/jobs/notional/notional.go @@ -0,0 +1,213 @@ +// Package notional contains the logic to get the notional value of assets +package notional + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/go-redis/redis" + "github.com/wormhole-foundation/wormhole-explorer/jobs/internal/coingecko" + "github.com/wormhole-foundation/wormhole/sdk/vaa" + "go.uber.org/zap" +) + +// NotionalCacheKey is the cache key for notional value by chainID +const NotionalCacheKey = "WORMSCAN:NOTIONAL:CHAIN_ID:%d" + +// NotionalJob is the job to get the notional value of assets. +type NotionalJob struct { + coingeckoAPI *coingecko.CoingeckoAPI + cacheClient *redis.Client + cacheChannel string + p2pNetwork string + logger *zap.Logger +} + +// NewNotionalJob creates a new notional job. +func NewNotionalJob(api *coingecko.CoingeckoAPI, cacheClient *redis.Client, p2pNetwork, cacheChannel string, logger *zap.Logger) *NotionalJob { + return &NotionalJob{ + coingeckoAPI: api, + cacheClient: cacheClient, + cacheChannel: cacheChannel, + p2pNetwork: p2pNetwork, + logger: logger, + } +} + +// Run runs the notional job. +func (j *NotionalJob) Run() error { + // get chains coingecko ids by p2p network. + chainIDs := coingecko.GetChainIDs(j.p2pNetwork) + if len(chainIDs) == 0 { + return fmt.Errorf("no chain ids found for p2p network %s", j.p2pNetwork) + } + + // get notional value of assets. + coingeckoNotionals, err := j.coingeckoAPI.GetNotionalUSD(chainIDs) + if err != nil { + j.logger.Error("failed to get notional value of assets", + zap.Error(err)) + return err + } + + // convert notionals with coingecko assets ids to notionals with wormhole chainIDs. + notionals := convertToWormholeChainIDs(coingeckoNotionals) + + // save notional value of assets in cache. + err = j.updateNotionalCache(notionals) + if err != nil { + j.logger.Error("failed to update notional value of assets in cache", + zap.Error(err), + zap.Any("notionals", notionals)) + return err + } + + // publish notional value of assets to redis pubsub. + err = j.cacheClient.Publish(j.cacheChannel, "NOTIONA_UPDATED").Err() + if err != nil { + j.logger.Error("failed to publish notional update message to redis pubsub", + zap.Error(err)) + return err + } + + return nil +} + +// updateNotionalCache updates the notional value of assets in cache. +func (j *NotionalJob) updateNotionalCache(notionals map[vaa.ChainID]NotionalCacheField) error { + for chainID, notional := range notionals { + key := fmt.Sprintf(NotionalCacheKey, chainID) + err := j.cacheClient.Set(key, notional, 0).Err() + if err != nil { + return err + } + } + return nil +} + +// NotionalCacheField is the notional value of assets in cache. +type NotionalCacheField struct { + NotionalUsd float64 `json:"notional_usd"` + UpdatedAt time.Time `json:"updated_at"` +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (n NotionalCacheField) MarshalBinary() ([]byte, error) { + return json.Marshal(n) +} + +// convertToWormholeChainIDs converts the coingecko chain ids to wormhole chain ids. +func convertToWormholeChainIDs(m map[string]coingecko.NotionalUSD) map[vaa.ChainID]NotionalCacheField { + w := make(map[vaa.ChainID]NotionalCacheField, len(m)) + now := time.Now() + for k, v := range m { + switch k { + case "solana": + if v.Price != nil { + w[vaa.ChainIDSolana] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "ethereum": + if v.Price != nil { + w[vaa.ChainIDEthereum] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "terra-luna": + if v.Price != nil { + w[vaa.ChainIDTerra] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "binancecoin": + if v.Price != nil { + w[vaa.ChainIDBSC] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "matic-network": + if v.Price != nil { + w[vaa.ChainIDPolygon] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "avalanche-2": + if v.Price != nil { + w[vaa.ChainIDAvalanche] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "oasis-network": + if v.Price != nil { + w[vaa.ChainIDOasis] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "algorand": + if v.Price != nil { + w[vaa.ChainIDAlgorand] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "aurora": + if v.Price != nil { + w[vaa.ChainIDAurora] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "fantom": + if v.Price != nil { + w[vaa.ChainIDFantom] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "karura": + if v.Price != nil { + w[vaa.ChainIDKarura] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "acala": + if v.Price != nil { + w[vaa.ChainIDAcala] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "klay-token": + if v.Price != nil { + w[vaa.ChainIDKlaytn] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "celo": + if v.Price != nil { + w[vaa.ChainIDCelo] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "near": + if v.Price != nil { + w[vaa.ChainIDNear] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "moonbeam": + if v.Price != nil { + w[vaa.ChainIDMoonbeam] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "neon": + if v.Price != nil { + w[vaa.ChainIDNeon] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "terra-luna-2": + if v.Price != nil { + w[vaa.ChainIDTerra2] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "injective-protocol": + if v.Price != nil { + w[vaa.ChainIDInjective] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "aptos": + if v.Price != nil { + w[vaa.ChainIDAptos] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "sui": + if v.Price != nil { + w[vaa.ChainIDSui] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "arbitrum": + if v.Price != nil { + w[vaa.ChainIDArbitrum] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "optimism": + if v.Price != nil { + w[vaa.ChainIDOptimism] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "xpla": + if v.Price != nil { + w[vaa.ChainIDXpla] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "bitcoin": + if v.Price != nil { + w[vaa.ChainIDBtc] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + case "base-protocol": + if v.Price != nil { + w[vaa.ChainIDBase] = NotionalCacheField{NotionalUsd: *v.Price, UpdatedAt: now} + } + } + } + return w +}