Add notional asset lookup (#239)

* Add notional job

* fix coingecko integration

* Add deployment for notional jobs

---------

Co-authored-by: Agustin Pazos <agpazos85@gmail.com>
This commit is contained in:
ftocal 2023-04-18 12:09:31 -03:00 committed by GitHub
parent 3c597917f5
commit 4f438ae188
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 672 additions and 0 deletions

13
deploy/jobs/env/production.env vendored Normal file
View File

@ -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 * * * *

13
deploy/jobs/env/staging.env vendored Normal file
View File

@ -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 * * * *

13
deploy/jobs/env/test.env vendored Normal file
View File

@ -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 * * * *

34
deploy/jobs/notional.yaml Normal file
View File

@ -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

View File

@ -10,4 +10,5 @@ use (
./pipeline
./spy
./tx-tracker
./jobs
)

21
jobs/Dockerfile Normal file
View File

@ -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"]

17
jobs/Makefile Normal file
View File

@ -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

2
jobs/README.md Normal file
View File

@ -0,0 +1,2 @@
# Jobs
This component contains the jobs to be scheduler.

67
jobs/cmd/main.go Normal file
View File

@ -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
}
}

33
jobs/config/config.go Normal file
View File

@ -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
}

26
jobs/go.mod Normal file
View File

@ -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
)

118
jobs/go.sum Normal file
View File

@ -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=

View File

@ -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{}
}

12
jobs/jobs/jobs.go Normal file
View File

@ -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
}

View File

@ -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
}