Commit f4f3054a authored by Conner Fromknecht's avatar Conner Fromknecht

feat: add teleportr API server

parent bced4fa9
---
'@eth-optimism/teleportr': patch
---
Add teleportr API server
...@@ -13,8 +13,12 @@ DISBURSER_ARTIFACT := ../../packages/contracts/artifacts/contracts/L2/teleportr/ ...@@ -13,8 +13,12 @@ DISBURSER_ARTIFACT := ../../packages/contracts/artifacts/contracts/L2/teleportr/
teleportr: teleportr:
env GO111MODULE=on go build -v $(LDFLAGS) ./cmd/teleportr env GO111MODULE=on go build -v $(LDFLAGS) ./cmd/teleportr
teleportr-api:
env GO111MODULE=on go build -v $(LDFLAGS) ./cmd/teleportr-api
clean: clean:
rm teleportr rm teleportr
rm api
test: test:
go test -v ./... go test -v ./...
...@@ -48,6 +52,7 @@ bindings-disburser: ...@@ -48,6 +52,7 @@ bindings-disburser:
.PHONY: \ .PHONY: \
teleportr \ teleportr \
teleportr-api \
bindings \ bindings \
bindings-deposit \ bindings-deposit \
bindings-disburser \ bindings-disburser \
......
package api
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
const TeleportrAPINamespace = "teleportr_api"
var (
rpcRequestsTotal = promauto.NewCounter(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "rpc_requests_total",
Help: "Count of total client RPC requests.",
})
httpResponseCodesTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "http_response_codes_total",
Help: "Count of total HTTP response codes.",
}, []string{
"status_code",
})
httpRequestDurationSumm = promauto.NewSummary(prometheus.SummaryOpts{
Namespace: TeleportrAPINamespace,
Name: "http_request_duration_seconds",
Help: "Summary of HTTP request durations, in seconds.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.005, 0.99: 0.001},
})
databaseErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "database_errors_total",
Help: "Count of total database failures.",
}, []string{
"method",
})
rpcErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: TeleportrAPINamespace,
Name: "rpc_errors_total",
Help: "Count of total L1 rpc failures.",
}, []string{
"method",
})
)
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"sync"
"syscall"
"time"
bsscore "github.com/ethereum-optimism/optimism/go/bss-core"
"github.com/ethereum-optimism/optimism/go/bss-core/dial"
"github.com/ethereum-optimism/optimism/go/bss-core/txmgr"
"github.com/ethereum-optimism/optimism/go/teleportr/bindings/deposit"
"github.com/ethereum-optimism/optimism/go/teleportr/db"
"github.com/ethereum-optimism/optimism/go/teleportr/flags"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
"github.com/rs/cors"
"github.com/urfave/cli"
)
type ContextKey string
const (
ContextKeyReqID ContextKey = "req_id"
)
func Main(gitVersion string) func(*cli.Context) error {
return func(cliCtx *cli.Context) error {
cfg, err := NewConfig(cliCtx)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
depositAddr, err := bsscore.ParseAddress(cfg.DepositAddress)
if err != nil {
return err
}
l1Client, err := dial.L1EthClientWithTimeout(
ctx, cfg.L1EthRpc, cfg.DisableHTTP2,
)
if err != nil {
return err
}
defer l1Client.Close()
depositContract, err := deposit.NewTeleportrDeposit(
depositAddr, l1Client,
)
if err != nil {
return err
}
// TODO(conner): make read-only
database, err := db.Open(db.Config{
Host: cfg.PostgresHost,
Port: uint16(cfg.PostgresPort),
User: cfg.PostgresUser,
Password: cfg.PostgresPassword,
DBName: cfg.PostgresDBName,
EnableSSL: cfg.PostgresEnableSSL,
})
if err != nil {
return err
}
defer database.Close()
server := NewServer(
ctx,
l1Client,
database,
depositAddr,
depositContract,
cfg.NumConfirmations,
)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
_ = server.ListenAndServe(cfg.Hostname, cfg.Port)
}()
interruptChannel := make(chan os.Signal, 1)
signal.Notify(interruptChannel, []os.Signal{
os.Interrupt,
os.Kill,
syscall.SIGTERM,
syscall.SIGQUIT,
}...)
select {
case <-interruptChannel:
_ = server.httpServer.Shutdown(ctx)
time.AfterFunc(defaultTimeout, func() {
cancel()
_ = server.httpServer.Close()
})
wg.Wait()
case <-ctx.Done():
}
return nil
}
}
type Config struct {
Hostname string
Port uint16
L1EthRpc string
DepositAddress string
NumConfirmations uint64
PostgresHost string
PostgresPort uint16
PostgresUser string
PostgresPassword string
PostgresDBName string
PostgresEnableSSL bool
DisableHTTP2 bool
}
func NewConfig(ctx *cli.Context) (Config, error) {
return Config{
Hostname: ctx.GlobalString(flags.APIHostnameFlag.Name),
Port: uint16(ctx.GlobalUint64(flags.APIPortFlag.Name)),
L1EthRpc: ctx.GlobalString(flags.APIL1EthRpcFlag.Name),
DepositAddress: ctx.GlobalString(flags.APIDepositAddressFlag.Name),
NumConfirmations: ctx.GlobalUint64(flags.APINumConfirmationsFlag.Name),
PostgresHost: ctx.GlobalString(flags.APIPostgresHostFlag.Name),
PostgresPort: uint16(ctx.GlobalUint64(flags.APIPostgresPortFlag.Name)),
PostgresUser: ctx.GlobalString(flags.APIPostgresUserFlag.Name),
PostgresPassword: ctx.GlobalString(flags.APIPostgresPasswordFlag.Name),
PostgresDBName: ctx.GlobalString(flags.APIPostgresDBNameFlag.Name),
PostgresEnableSSL: ctx.GlobalBool(flags.APIPostgresEnableSSLFlag.Name),
}, nil
}
const (
ContentTypeHeader = "Content-Type"
ContentTypeJSON = "application/json"
defaultTimeout = 10 * time.Second
)
type Server struct {
ctx context.Context
l1Client *ethclient.Client
database *db.Database
depositAddr common.Address
depositContract *deposit.TeleportrDeposit
numConfirmations uint64
httpServer *http.Server
}
func NewServer(
ctx context.Context,
l1Client *ethclient.Client,
database *db.Database,
depositAddr common.Address,
depositContract *deposit.TeleportrDeposit,
numConfirmations uint64,
) *Server {
if numConfirmations == 0 {
panic("NumConfirmations cannot be zero")
}
return &Server{
ctx: ctx,
l1Client: l1Client,
database: database,
depositAddr: depositAddr,
depositContract: depositContract,
numConfirmations: numConfirmations,
}
}
func (s *Server) ListenAndServe(host string, port uint16) error {
handler := mux.NewRouter()
handler.HandleFunc("/healthz", HandleHealthz).Methods("GET")
handler.HandleFunc(
"/status",
instrumentedErrorHandler(s.HandleStatus),
).Methods("GET")
handler.HandleFunc(
"/estimate/{addr:0x[0-9a-fA-F]{40}}/{amount:[0-9]{1,80}}",
instrumentedErrorHandler(s.HandleEstimate),
).Methods("GET")
handler.HandleFunc(
"/track/{txhash:0x[0-9a-fA-F]{64}}",
instrumentedErrorHandler(s.HandleTrack),
).Methods("GET")
handler.HandleFunc(
"/history/{addr:0x[0-9a-fA-F]{40}}",
instrumentedErrorHandler(s.HandleHistory),
).Methods("GET")
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
})
addr := fmt.Sprintf("%s:%d", host, port)
s.httpServer = &http.Server{
Handler: c.Handler(handler),
Addr: addr,
BaseContext: func(_ net.Listener) context.Context {
return s.ctx
},
}
log.Info("Starting HTTP server", "addr", addr)
return s.httpServer.ListenAndServe()
}
func HandleHealthz(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("OK"))
}
type StatusResponse struct {
CurrentBalanceWei string `json:"current_balance_wei"`
MaximumBalanceWei string `json:"maximum_balance_wei"`
MinDepositAmountWei string `json:"min_deposit_amount_wei"`
MaxDepositAmountWei string `json:"max_deposit_amount_wei"`
IsAvailable bool `json:"is_available"`
}
func (s *Server) HandleStatus(
ctx context.Context,
w http.ResponseWriter,
r *http.Request,
) error {
maxBalance, err := s.depositContract.MaxBalance(&bind.CallOpts{
Context: ctx,
})
if err != nil {
rpcErrorsTotal.WithLabelValues("max_balance").Inc()
return err
}
minDepositAmount, err := s.depositContract.MinDepositAmount(&bind.CallOpts{
Context: ctx,
})
if err != nil {
rpcErrorsTotal.WithLabelValues("min_deposit_amount").Inc()
return err
}
maxDepositAmount, err := s.depositContract.MaxDepositAmount(&bind.CallOpts{
Context: ctx,
})
if err != nil {
rpcErrorsTotal.WithLabelValues("max_deposit_amount").Inc()
return err
}
curBalance, err := s.l1Client.BalanceAt(ctx, s.depositAddr, nil)
if err != nil {
rpcErrorsTotal.WithLabelValues("balance_at").Inc()
return err
}
balanceAfterMaxDeposit := new(big.Int).Add(
curBalance, maxDepositAmount,
)
isAvailable := curBalance.Cmp(balanceAfterMaxDeposit) >= 0
resp := StatusResponse{
CurrentBalanceWei: curBalance.String(),
MaximumBalanceWei: maxBalance.String(),
MinDepositAmountWei: minDepositAmount.String(),
MaxDepositAmountWei: maxDepositAmount.String(),
IsAvailable: isAvailable,
}
jsonResp, err := json.Marshal(resp)
if err != nil {
return err
}
w.Header().Set(ContentTypeHeader, ContentTypeJSON)
_, err = w.Write(jsonResp)
return err
}
type EstimateResponse struct {
BaseFee string `json:"base_fee"`
GasTipCap string `json:"gas_tip_cap"`
GasFeeCap string `json:"gas_fee_cap"`
GasEstimate string `json:"gas_estimate"`
}
func (s *Server) HandleEstimate(
ctx context.Context,
w http.ResponseWriter,
r *http.Request,
) error {
vars := mux.Vars(r)
addressStr, ok := vars["addr"]
if !ok {
return StatusError{
Err: errors.New("missing address parameter"),
Code: http.StatusBadRequest,
}
}
address, err := bsscore.ParseAddress(addressStr)
if err != nil {
return StatusError{
Err: err,
Code: http.StatusBadRequest,
}
}
amountStr, ok := vars["amount"]
if !ok {
return StatusError{
Err: errors.New("missing amount parameter"),
Code: http.StatusBadRequest,
}
}
amount, ok := new(big.Int).SetString(amountStr, 10)
if !ok {
return StatusError{
Err: errors.New("unable to parse amount"),
Code: http.StatusBadRequest,
}
}
gasTipCap, err := s.l1Client.SuggestGasTipCap(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("suggest_gas_tip_cap").Inc()
return err
}
header, err := s.l1Client.HeaderByNumber(ctx, nil)
if err != nil {
rpcErrorsTotal.WithLabelValues("header_by_number").Inc()
return err
}
gasFeeCap := txmgr.CalcGasFeeCap(header.BaseFee, gasTipCap)
gasUsed, err := s.l1Client.EstimateGas(ctx, ethereum.CallMsg{
From: address,
To: &s.depositAddr,
Gas: 0,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Value: amount,
Data: nil,
})
if err != nil {
rpcErrorsTotal.WithLabelValues("estimate_gas").Inc()
return err
}
resp := EstimateResponse{
BaseFee: header.BaseFee.String(),
GasTipCap: gasTipCap.String(),
GasFeeCap: gasFeeCap.String(),
GasEstimate: new(big.Int).SetUint64(gasUsed).String(),
}
jsonResp, err := json.Marshal(resp)
if err != nil {
return err
}
w.Header().Set(ContentTypeHeader, ContentTypeJSON)
_, err = w.Write(jsonResp)
return err
}
type RPCTeleport struct {
ID string `json:"id"`
Address string `json:"address"`
AmountWei string `json:"amount_wei"`
TxHash string `json:"tx_hash"`
BlockNumber string `json:"block_number"`
BlockTimestamp string `json:"block_timestamp_unix"`
Disbursement *RPCDisbursement `json:"disbursement,omitempty"`
}
func makeRPCTeleport(teleport *db.Teleport) RPCTeleport {
rpcTeleport := RPCTeleport{
ID: strconv.FormatUint(teleport.ID, 10),
Address: teleport.Address.String(),
AmountWei: teleport.Amount.String(),
TxHash: teleport.Deposit.TxnHash.String(),
BlockNumber: strconv.FormatUint(teleport.Deposit.BlockNumber, 10),
BlockTimestamp: strconv.FormatInt(teleport.Deposit.BlockTimestamp.Unix(), 10),
}
if rpcTeleport.Disbursement != nil {
rpcTeleport.Disbursement = &RPCDisbursement{
TxHash: teleport.Disbursement.TxnHash.String(),
BlockNumber: strconv.FormatUint(teleport.Disbursement.BlockNumber, 10),
BlockTimestamp: strconv.FormatInt(teleport.Disbursement.BlockTimestamp.Unix(), 10),
Success: teleport.Disbursement.Success,
}
}
return rpcTeleport
}
type RPCDisbursement struct {
TxHash string `json:"tx_hash"`
BlockNumber string `json:"block_number"`
BlockTimestamp string `json:"block_timestamp_unix"`
Success bool `json:"success"`
}
type TrackResponse struct {
CurrentBlockNumber string `json:"current_block_number"`
ConfirmationsRequired string `json:"confirmations_required"`
ConfirmationsRemaining string `json:"confirmations_remaining"`
Teleport RPCTeleport
}
func (s *Server) HandleTrack(
ctx context.Context,
w http.ResponseWriter,
r *http.Request,
) error {
vars := mux.Vars(r)
txHashStr, ok := vars["txhash"]
if !ok {
return StatusError{
Err: errors.New("missing txhash parameter"),
Code: http.StatusBadRequest,
}
}
txHash := common.HexToHash(txHashStr)
blockNumber, err := s.l1Client.BlockNumber(ctx)
if err != nil {
rpcErrorsTotal.WithLabelValues("block_number").Inc()
return err
}
teleport, err := s.database.LoadTeleportByDepositHash(txHash)
if err != nil {
databaseErrorsTotal.WithLabelValues("load_teleport_by_deposit_hash").Inc()
return err
}
if teleport == nil {
return StatusError{
Code: http.StatusNotFound,
}
}
var confsRemaining uint64
if teleport.Deposit.BlockNumber+s.numConfirmations > blockNumber+1 {
confsRemaining = blockNumber + 1 -
(teleport.Deposit.BlockNumber + s.numConfirmations)
}
resp := TrackResponse{
CurrentBlockNumber: strconv.FormatUint(blockNumber, 10),
ConfirmationsRequired: strconv.FormatUint(s.numConfirmations, 10),
ConfirmationsRemaining: strconv.FormatUint(confsRemaining, 10),
Teleport: makeRPCTeleport(teleport),
}
jsonResp, err := json.Marshal(resp)
if err != nil {
return err
}
w.Header().Set(ContentTypeHeader, ContentTypeJSON)
_, err = w.Write(jsonResp)
return err
}
type HistoryResponse struct {
Teleports []RPCTeleport `json:"teleports"`
}
func (s *Server) HandleHistory(
ctx context.Context,
w http.ResponseWriter,
r *http.Request,
) error {
vars := mux.Vars(r)
addrStr, ok := vars["addr"]
if !ok {
return StatusError{
Err: errors.New("missing addr parameter"),
Code: http.StatusBadRequest,
}
}
addr := common.HexToAddress(addrStr)
teleports, err := s.database.LoadTeleportsByAddress(addr)
if err != nil {
databaseErrorsTotal.WithLabelValues("load_teleports_by_address").Inc()
return err
}
rpcTeleports := make([]RPCTeleport, 0, len(teleports))
for _, teleport := range teleports {
rpcTeleports = append(rpcTeleports, makeRPCTeleport(&teleport))
}
resp := HistoryResponse{
Teleports: rpcTeleports,
}
jsonResp, err := json.Marshal(resp)
if err != nil {
return err
}
w.Header().Set(ContentTypeHeader, ContentTypeJSON)
_, err = w.Write(jsonResp)
return err
}
type Error interface {
error
Status() int
}
type StatusError struct {
Code int
Err error
}
func (se StatusError) Error() string {
if se.Err != nil {
msg := se.Err.Error()
if msg != "" {
return msg
}
}
return http.StatusText(se.Code)
}
func (se StatusError) Status() int {
return se.Code
}
func instrumentedErrorHandler(
h func(context.Context, http.ResponseWriter, *http.Request) error,
) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
rpcRequestsTotal.Inc()
ctx, cancel := populateContext(w, r)
defer cancel()
reqID := GetReqID(ctx)
log.Info("HTTP request",
"req_id", reqID,
"path", r.URL.Path,
"user_agent", r.UserAgent())
respTimer := prometheus.NewTimer(httpRequestDurationSumm)
err := h(ctx, w, r)
elapsed := respTimer.ObserveDuration()
var statusCode int
switch e := err.(type) {
case nil:
statusCode = 200
log.Info("HTTP success",
"req_id", reqID,
"elapsed", elapsed)
case Error:
statusCode = e.Status()
log.Warn("HTTP error",
"req_id", reqID,
"elapsed", elapsed,
"status", statusCode,
"err", e.Error())
http.Error(w, e.Error(), statusCode)
default:
statusCode = http.StatusInternalServerError
log.Warn("HTTP internal error",
"req_id", reqID,
"elapsed", elapsed,
"status", statusCode,
"err", err)
http.Error(w, http.StatusText(statusCode), statusCode)
}
httpResponseCodesTotal.WithLabelValues(strconv.Itoa(statusCode)).Inc()
}
}
func populateContext(
w http.ResponseWriter,
r *http.Request,
) (context.Context, func()) {
ctx := context.WithValue(r.Context(), ContextKeyReqID, uuid.NewString())
return context.WithTimeout(ctx, defaultTimeout)
}
func GetReqID(ctx context.Context) string {
if reqID, ok := ctx.Value(ContextKeyReqID).(string); ok {
return reqID
}
return ""
}
package main
import (
"fmt"
"os"
"github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli"
"github.com/ethereum-optimism/optimism/go/teleportr/api"
"github.com/ethereum-optimism/optimism/go/teleportr/flags"
)
var (
GitVersion = ""
GitCommit = ""
GitDate = ""
)
func main() {
// Set up logger with a default INFO level in case we fail to parse flags.
// Otherwise the final critical log won't show what the parsing error was.
log.Root().SetHandler(
log.LvlFilterHandler(
log.LvlInfo,
log.StreamHandler(os.Stdout, log.TerminalFormat(true)),
),
)
app := cli.NewApp()
app.Flags = flags.APIFlags
app.Version = fmt.Sprintf("%s-%s-%s", GitVersion, GitCommit, GitDate)
app.Name = "teleportr-api"
app.Usage = "Teleportr API server"
app.Description = "API serving teleportr data"
app.Action = api.Main(GitVersion)
err := app.Run(os.Args)
if err != nil {
log.Crit("Application failed", "message", err)
}
}
package flags
import (
"fmt"
"strings"
"github.com/urfave/cli"
)
func prefixAPIEnvVar(name string) string {
return fmt.Sprintf("TELEPORTR_API_%s", strings.ToUpper(name))
}
var (
APIHostnameFlag = cli.StringFlag{
Name: "hostname",
Usage: "The hostname of the API server",
Required: true,
EnvVar: prefixAPIEnvVar("HOSTNAME"),
}
APIPortFlag = cli.StringFlag{
Name: "port",
Usage: "The hostname of the API server",
Required: true,
EnvVar: prefixAPIEnvVar("PORT"),
}
APIL1EthRpcFlag = cli.StringFlag{
Name: "l1-eth-rpc",
Usage: "The endpoint for the L1 ETH provider",
Required: true,
EnvVar: prefixAPIEnvVar("L1_ETH_RPC"),
}
APIDepositAddressFlag = cli.StringFlag{
Name: "deposit-address",
Usage: "Address of the TeleportrDeposit contract",
Required: true,
EnvVar: prefixAPIEnvVar("DEPOSIT_ADDRESS"),
}
APINumConfirmationsFlag = cli.StringFlag{
Name: "num-confirmations",
Usage: "Number of confirmations required until deposits are " +
"considered confirmed",
Required: true,
EnvVar: prefixAPIEnvVar("NUM_CONFIRMATIONS"),
}
APIPostgresHostFlag = cli.StringFlag{
Name: "postgres-host",
Usage: "Host of the teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_HOST"),
}
APIPostgresPortFlag = cli.Uint64Flag{
Name: "postgres-port",
Usage: "Port of the teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_PORT"),
}
APIPostgresUserFlag = cli.StringFlag{
Name: "postgres-user",
Usage: "Username of the teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_USER"),
}
APIPostgresPasswordFlag = cli.StringFlag{
Name: "postgres-password",
Usage: "Password of the teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_PASSWORD"),
}
APIPostgresDBNameFlag = cli.StringFlag{
Name: "postgres-db-name",
Usage: "Database name of the teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_DB_NAME"),
}
APIPostgresEnableSSLFlag = cli.BoolFlag{
Name: "postgres-enable-ssl",
Usage: "Whether or not to enable SSL on connections to " +
"teleportr postgres instance",
Required: true,
EnvVar: prefixAPIEnvVar("POSTGRES_ENABLE_SSL"),
}
)
var APIFlags = []cli.Flag{
APIHostnameFlag,
APIPortFlag,
APIL1EthRpcFlag,
APIDepositAddressFlag,
APINumConfirmationsFlag,
APIPostgresHostFlag,
APIPostgresPortFlag,
APIPostgresUserFlag,
APIPostgresPasswordFlag,
APIPostgresDBNameFlag,
APIPostgresEnableSSLFlag,
}
...@@ -6,7 +6,10 @@ require ( ...@@ -6,7 +6,10 @@ require (
github.com/ethereum-optimism/optimism/go/bss-core v0.0.0-20220218171106-67a0414d7606 github.com/ethereum-optimism/optimism/go/bss-core v0.0.0-20220218171106-67a0414d7606
github.com/ethereum/go-ethereum v1.10.15 github.com/ethereum/go-ethereum v1.10.15
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/lib/pq v1.10.4 github.com/lib/pq v1.10.4
github.com/prometheus/client_golang v1.11.0
github.com/rs/cors v1.7.0
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/urfave/cli v1.22.5 github.com/urfave/cli v1.22.5
) )
...@@ -39,7 +42,6 @@ require ( ...@@ -39,7 +42,6 @@ require (
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect
......
...@@ -265,6 +265,7 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ ...@@ -265,6 +265,7 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment