-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
initial code for faucet service (#232)
* initial code for faucet service * create proto stubs for faucet * fix faucet to be runable service * add workflow to build faucet * start creating the structure for faucet and distributors of funds * update config to add faucet based arguments, add logic for distributors * initialize distributor into the appserver * add default values to the config * update default ports for faucet * rename genesis key to holder for distributor * udpate makefile * add initial working model with adding keys via distributor * getting refill and distributor working, not working yet * fix send tokens, update code * run distributor as its own go-routine * add /status endpoint to return proto defination * hook up /credit endpoint to distributor send tokens, seems to work * add docker build to ci, cleanup makefile * add more config validation * fix imports * move parsing of coin from config to coin.go * restructure coin.go * fix typos, fix lints, cleanup
- Loading branch information
Showing
23 changed files
with
2,352 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# If you prefer the allow list template instead of the deny list, see community template: | ||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore | ||
# | ||
# Binaries for programs and plugins | ||
*.exe | ||
*.exe~ | ||
*.dll | ||
*.so | ||
*.dylib | ||
|
||
# Test binary, built with `go test -c` | ||
*.test | ||
|
||
# Output of the go coverage tool, specifically when used with LiteIDE | ||
*.out | ||
|
||
# Dependency directories (remove the comment below to include it) | ||
# vendor/ | ||
|
||
# Go workspace file | ||
go.work | ||
|
||
bin/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
FROM golang:1.19-alpine3.16 AS builder | ||
|
||
LABEL org.opencontainers.image.source="https://github.com/cosmology-tech/starship" | ||
|
||
WORKDIR /usr/local/app | ||
|
||
COPY go.mod . | ||
RUN go mod download | ||
|
||
COPY . . | ||
RUN CGO_ENABLED=0 go build -mod=readonly -o build/ ./... | ||
|
||
FROM ghcr.io/cosmology-tech/starship/base:latest | ||
|
||
COPY --from=builder /usr/local/app/build/faucet /bin | ||
|
||
WORKDIR /opt | ||
|
||
ENTRYPOINT ["faucet"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
BINARY_NAME = faucet | ||
|
||
DOCKER := $(shell which docker) | ||
# DOCKER_REPO_NAME is the local docker repo used, can be set to individual dockerhub username | ||
DOCKER_REPO_NAME := starship | ||
DOCKER_IMAGE := faucet | ||
DOCKER_TAG_NAME := $(shell date '+%Y%m%d')-$(shell git rev-parse --short HEAD) | ||
|
||
all: build run | ||
|
||
.PHONY: build | ||
build: | ||
CGO_ENABLED=0 go build -mod=readonly -o $(CURDIR)/build/ ./... | ||
|
||
.PHONY: build-linux | ||
build-linux: | ||
GOOS=linux GOARCH=amd64 $(MAKE) build | ||
|
||
.PHONY: build-arm | ||
build-arm: | ||
GOOS=linux GOARCH=arm64 $(MAKE) build | ||
|
||
## Need to be running osmosis node at localhost:1313 and 26653 | ||
run-docker: build-arm | ||
docker run --rm -v $(CURDIR)/build:/build -p 8800:8000 -it ghcr.io/cosmology-tech/starship/osmosis:v15.1.0 /build/faucet \ | ||
--mnemonic="razor dog gown public private couple ecology paper flee connect local robot diamond stay rude join sound win ribbon soup kidney glass robot vehicle" \ | ||
--chain-binary="osmosisd" \ | ||
--concurrency=11 \ | ||
--credit-coins="10000000uosmo,10000000uion" \ | ||
--chain-id="osmosis-1" \ | ||
--chain-rest-endpoint="http://host.docker.internal:1313" \ | ||
--chain-rpc-endpoint="http://host.docker.internal:26653" \ | ||
--chain-fees="10000uosmo" | ||
|
||
test-credit: | ||
curl --header "Content-Type: application/json" \ | ||
--request POST --data '{"denom":"uosmo","address":"osmo1tkrwspedcqwm6ve8vtk0vuzgr4c0m5203era0x"}' http://localhost:8800/credit | ||
|
||
## Docker commands | ||
docker-setup: | ||
-docker buildx rm starship | ||
docker buildx create --use --name starship | ||
|
||
docker-build: | ||
$(DOCKER) buildx build . --platform linux/amd64,linux/arm64 -t $(DOCKER_REPO_NAME)/$(DOCKER_IMAGE):$(DOCKER_TAG_NAME) | ||
|
||
docker-build-push: | ||
$(DOCKER) buildx build . --platform linux/amd64,linux/arm64 -t $(DOCKER_REPO_NAME)/$(DOCKER_IMAGE):$(DOCKER_TAG_NAME) --push | ||
|
||
docker-run: | ||
$(DOCKER) run --rm -it --entrypoint /bin/bash $(DOCKER_REPO_NAME)/$(DOCKER_IMAGE):$(DOCKER_TAG_NAME) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net" | ||
"net/http" | ||
"os/exec" | ||
"time" | ||
|
||
"github.com/go-chi/chi/middleware" | ||
grpcmiddleware "github.com/grpc-ecosystem/go-grpc-middleware" | ||
grpczap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" | ||
grpcrecovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" | ||
grpcctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags" | ||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime" | ||
"go.uber.org/zap" | ||
"google.golang.org/grpc" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
|
||
pb "github.com/cosmology-tech/starship/faucet/faucet" | ||
) | ||
|
||
type AppServer struct { | ||
pb.UnimplementedFaucetServer | ||
|
||
config *Config | ||
logger *zap.Logger | ||
|
||
distributor *Distributor | ||
|
||
grpcServer *grpc.Server | ||
httpServer *http.Server | ||
} | ||
|
||
func NewAppServer(config *Config) (*AppServer, error) { | ||
log, err := NewLogger(config) | ||
if err != nil { | ||
return nil, err | ||
} | ||
log.Info( | ||
"Starting the service", | ||
zap.String("prog", Prog), | ||
zap.String("version", Version), | ||
zap.Any("config", config), | ||
) | ||
|
||
app := &AppServer{ | ||
config: config, | ||
logger: log, | ||
} | ||
|
||
// Validate config | ||
err = app.ValidateConfig() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Create distributor for manage keys and accounts | ||
distributor, err := NewDistributor(config, log) | ||
if err != nil { | ||
return nil, err | ||
} | ||
app.distributor = distributor | ||
|
||
// Create grpc server | ||
grpcServer := grpc.NewServer(app.grpcMiddleware()...) | ||
pb.RegisterFaucetServer(grpcServer, app) | ||
app.grpcServer = grpcServer | ||
|
||
// Create http server | ||
mux := runtime.NewServeMux() | ||
err = pb.RegisterFaucetHandlerFromEndpoint( | ||
context.Background(), | ||
mux, | ||
fmt.Sprintf("%s:%s", config.Host, config.GRPCPort), | ||
[]grpc.DialOption{grpc.WithInsecure()}, | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
httpServer := &http.Server{ | ||
Addr: fmt.Sprintf("%s:%s", config.Host, config.HTTPPort), | ||
Handler: app.panicRecovery(app.loggingMiddleware(mux)), | ||
} | ||
app.httpServer = httpServer | ||
|
||
return app, err | ||
} | ||
|
||
func (a *AppServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
a.httpServer.Handler.ServeHTTP(w, r) | ||
} | ||
|
||
func (a *AppServer) ValidateConfig() error { | ||
// Verify config provided | ||
_, err := exec.LookPath(a.config.ChainBinary) | ||
if err != nil { | ||
return fmt.Errorf("chain binary '%s' error: %w", a.config.ChainBinary, err) | ||
} | ||
if a.config.ChainId == "" { | ||
return errors.New("chain id can not be empty") | ||
} | ||
if a.config.CreditCoins == "" { | ||
return errors.New("credit tokens can not be empty") | ||
} | ||
if a.config.ChainRPCEndpoint == "" { | ||
return errors.New("chain rpc endpoint can not be empty") | ||
} | ||
if a.config.ChainRESTEndpoint == "" { | ||
return errors.New("chain rest endpoint can not be empty") | ||
} | ||
if a.config.ChainFees == "" { | ||
return errors.New("chain fees can not be empty") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (a *AppServer) grpcMiddleware() []grpc.ServerOption { | ||
opts := []grpcrecovery.Option{ | ||
grpcrecovery.WithRecoveryHandler( | ||
func(p interface{}) error { | ||
err := status.Errorf(codes.Unknown, "panic triggered: %v", p) | ||
a.logger.Error("panic error", zap.Error(err)) | ||
return err | ||
}, | ||
), | ||
} | ||
|
||
serverOpts := []grpc.ServerOption{ | ||
grpcmiddleware.WithUnaryServerChain( | ||
grpcctxtags.UnaryServerInterceptor(), | ||
grpczap.UnaryServerInterceptor(a.logger), | ||
grpcrecovery.UnaryServerInterceptor(opts...), | ||
), | ||
grpcmiddleware.WithStreamServerChain( | ||
grpcctxtags.StreamServerInterceptor(), | ||
grpczap.StreamServerInterceptor(a.logger), | ||
grpcrecovery.StreamServerInterceptor(opts...), | ||
), | ||
} | ||
|
||
return serverOpts | ||
} | ||
|
||
func (a *AppServer) loggingMiddleware(next http.Handler) http.Handler { | ||
fn := func(w http.ResponseWriter, r *http.Request) { | ||
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) | ||
start := time.Now() | ||
defer func() { | ||
a.logger.Info("client request", | ||
zap.Duration("latency", time.Since(start)), | ||
zap.Int("status", ww.Status()), | ||
zap.Int("bytes", ww.BytesWritten()), | ||
zap.String("client_ip", r.RemoteAddr), | ||
zap.String("method", r.Method), | ||
zap.String("path", r.URL.Path), | ||
zap.String("request-id", middleware.GetReqID(r.Context()))) | ||
}() | ||
next.ServeHTTP(ww, r) | ||
} | ||
|
||
return http.HandlerFunc(fn) | ||
} | ||
|
||
func (a *AppServer) panicRecovery(next http.Handler) http.Handler { | ||
fn := func(w http.ResponseWriter, r *http.Request) { | ||
defer func() { | ||
if rc := recover(); rc != nil { | ||
err, ok := rc.(error) | ||
if !ok { | ||
err = fmt.Errorf("panic: %v", rc) | ||
} | ||
a.logger.Error("panic error", zap.Error(err)) | ||
|
||
http.Error(w, ErrInternalServer.Error(), 500) | ||
return | ||
} | ||
}() | ||
next.ServeHTTP(w, r) | ||
} | ||
|
||
return http.HandlerFunc(fn) | ||
} | ||
|
||
func (a *AppServer) Run() error { | ||
a.logger.Info("App starting", zap.Any("config", a.config)) | ||
|
||
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%s", a.config.Host, a.config.GRPCPort)) | ||
if err != nil { | ||
a.logger.Error("failed to listen", zap.Error(err)) | ||
} | ||
|
||
// Start grpc server as long-running go routine | ||
go func() { | ||
if err := a.grpcServer.Serve(lis); err != nil { | ||
a.logger.Error("failed to start the App gRPC server", zap.Error(err)) | ||
} | ||
}() | ||
|
||
// Start http server | ||
go func() { | ||
if err := a.httpServer.ListenAndServe(); err != nil { | ||
a.logger.Error("failed to start the App http server", zap.Error(err)) | ||
} | ||
}() | ||
|
||
// start distributor | ||
go func() { | ||
for { | ||
disStatus, err := a.distributor.Status() | ||
if err != nil { | ||
a.logger.Error("distributor error status", zap.Error(err)) | ||
} | ||
a.logger.Info("status of distributor", zap.Any("disStatus", disStatus)) | ||
err = a.distributor.Refill() | ||
if err != nil { | ||
a.logger.Error("distributor error refilling", zap.Error(err)) | ||
} | ||
time.Sleep(time.Duration(a.config.RefillEpoch) * time.Second) | ||
} | ||
}() | ||
|
||
return nil | ||
} |
Oops, something went wrong.