Skip to content

Commit

Permalink
initial code for faucet service (#232)
Browse files Browse the repository at this point in the history
* 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
Anmol1696 authored Sep 15, 2023
1 parent 1b0afa4 commit 1b6764f
Show file tree
Hide file tree
Showing 23 changed files with 2,352 additions and 2 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,19 @@ jobs:
- name: Build registry
run: |
cd registry && make build
build-faucet:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v2

- uses: actions/setup-go@v4
with:
go-version: "1.19"
check-latest: true

- name: Build faucet
run: |
cd faucet && make build
2 changes: 1 addition & 1 deletion .github/workflows/starship-docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:

strategy:
matrix:
type: [ "registry", "exposer" ]
type: [ "registry", "exposer", "faucet" ]
fail-fast: false

env:
Expand Down
23 changes: 23 additions & 0 deletions faucet/.gitignore
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/
19 changes: 19 additions & 0 deletions faucet/Dockerfile
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"]
51 changes: 51 additions & 0 deletions faucet/Makefile
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)
228 changes: 228 additions & 0 deletions faucet/app.go
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
}
Loading

0 comments on commit 1b6764f

Please sign in to comment.