Skip to content

Commit eb513e0

Browse files
authored
Merge pull request #1 from cloudstruct/feature/initial
Initial implementation
2 parents 03e3d0e + 764aefa commit eb513e0

File tree

10 files changed

+479
-0
lines changed

10 files changed

+479
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# This file was copied from the following URL and modified:
2+
# https://github.com/golangci/golangci-lint-action/blob/master/README.md#how-to-use
3+
4+
name: golangci-lint
5+
on:
6+
push:
7+
tags:
8+
- v*
9+
branches:
10+
- master
11+
- main
12+
pull_request:
13+
permissions:
14+
contents: read
15+
# Optional: allow read access to pull request. Use with `only-new-issues` option.
16+
# pull-requests: read
17+
jobs:
18+
golangci:
19+
name: lint
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v2
23+
- uses: actions/setup-go@v3
24+
with:
25+
go-version: '1.17'
26+
- name: golangci-lint
27+
uses: golangci/golangci-lint-action@v3
28+
with:
29+
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
30+
version: v1.45.2 # current version at time of commit
31+
32+
# Optional: working directory, useful for monorepos
33+
# working-directory: somedir
34+
35+
# Optional: golangci-lint command line arguments.
36+
# args: --issues-exit-code=0
37+
38+
# Optional: show only new issues if it's a pull request. The default value is `false`.
39+
# only-new-issues: true
40+
41+
# Optional: if set to true then the action don't cache or restore ~/go/pkg.
42+
# skip-pkg-cache: true
43+
44+
# Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
45+
# skip-build-cache: true

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@
1313

1414
# Dependency directories (remove the comment below to include it)
1515
# vendor/
16+
17+
# Binary
18+
/tx-submit-api-mirror

Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM golang:1.17 AS build
2+
3+
COPY . /code
4+
5+
WORKDIR /code
6+
7+
RUN make build
8+
9+
FROM ubuntu:bionic
10+
11+
COPY --from=build /code/tx-submit-api-mirror /usr/local/bin/
12+
13+
ENTRYPOINT ["/usr/local/bin/tx-submit-api-mirror"]

Makefile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
BINARY=tx-submit-api-mirror
2+
3+
# Determine root directory
4+
ROOT_DIR=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
5+
6+
# Gather all .go files for use in dependencies below
7+
GO_FILES=$(shell find $(ROOT_DIR) -name '*.go')
8+
9+
# Build our program binary
10+
# Depends on GO_FILES to determine when rebuild is needed
11+
$(BINARY): $(GO_FILES)
12+
# Needed to fetch new dependencies and add them to go.mod
13+
go mod tidy
14+
go build -o $(BINARY) ./cmd/$(BINARY)
15+
16+
.PHONY: build image
17+
18+
# Alias for building program binary
19+
build: $(BINARY)
20+
21+
# Build docker image
22+
image: build
23+
docker build -t cloudstruct/$(BINARY) .

api/api.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package api
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"fmt"
7+
"github.com/cloudstruct/tx-submit-api-mirror/config"
8+
"github.com/cloudstruct/tx-submit-api-mirror/logging"
9+
"github.com/fxamacker/cbor/v2"
10+
ginzap "github.com/gin-contrib/zap"
11+
"github.com/gin-gonic/gin"
12+
"golang.org/x/crypto/blake2b"
13+
"io/ioutil"
14+
"net/http"
15+
)
16+
17+
func Start(cfg *config.Config) error {
18+
// Disable gin debug output
19+
gin.SetMode(gin.ReleaseMode)
20+
gin.DisableConsoleColor()
21+
22+
// Configure router
23+
router := gin.New()
24+
// Catch panics and return a 500
25+
router.Use(gin.Recovery())
26+
// Access logging
27+
accessLogger := logging.GetAccessLogger()
28+
router.Use(ginzap.Ginzap(accessLogger, "", true))
29+
router.Use(ginzap.RecoveryWithZap(accessLogger, true))
30+
31+
// Configure routes
32+
router.GET("/healthcheck", handleHealthcheck)
33+
router.POST("/api/submit/tx", handleSubmitTx)
34+
35+
// Start listener
36+
err := router.Run(fmt.Sprintf("%s:%d", cfg.Api.ListenAddress, cfg.Api.ListenPort))
37+
return err
38+
}
39+
40+
func handleHealthcheck(c *gin.Context) {
41+
// TODO: add some actual health checking here
42+
c.JSON(200, gin.H{"failed": false})
43+
}
44+
45+
func handleSubmitTx(c *gin.Context) {
46+
cfg := config.GetConfig()
47+
logger := logging.GetLogger()
48+
if len(cfg.Backends) == 0 {
49+
logger.Errorf("no backends configured")
50+
c.String(500, "no backends configured")
51+
return
52+
}
53+
// Read transaction from request body
54+
rawTx, err := ioutil.ReadAll(c.Request.Body)
55+
if err != nil {
56+
logger.Errorf("failed to read request body: %s", err)
57+
c.String(500, "failed to request body")
58+
return
59+
}
60+
if err := c.Request.Body.Close(); err != nil {
61+
logger.Errorf("failed to close request body: %s", err)
62+
}
63+
logger.Debugf("transaction dump: %x", rawTx)
64+
// Unwrap transaction and calculate ID
65+
var txUnwrap []cbor.RawMessage
66+
if err := cbor.Unmarshal(rawTx, &txUnwrap); err != nil {
67+
logger.Errorf("failed to unwrap transaction CBOR: %s", err)
68+
c.String(400, fmt.Sprintf("failed to unwrap transaction CBOR: %s", err))
69+
return
70+
}
71+
txId := blake2b.Sum256(txUnwrap[0])
72+
txIdHex := hex.EncodeToString(txId[:])
73+
logger.Debugf("calculated transaction ID %s", txIdHex)
74+
// Send request to each backend
75+
for _, backend := range cfg.Backends {
76+
go func(backend string) {
77+
body := bytes.NewBuffer(rawTx)
78+
resp, err := http.Post(backend, "application/cbor", body)
79+
if err != nil {
80+
logger.Errorf("failed to send request to backend %s: %s", backend, err)
81+
return
82+
}
83+
if resp.StatusCode == 202 {
84+
logger.Infof("successfully submitted transaction %s to backend %s", txIdHex, backend)
85+
} else {
86+
respBody, err := ioutil.ReadAll(resp.Body)
87+
if err != nil {
88+
logger.Errorf("failed to read response body: %s", err)
89+
return
90+
}
91+
logger.Errorf("failed to send request to backend %s: got response %d, %s", backend, resp.StatusCode, string(respBody))
92+
}
93+
}(backend)
94+
}
95+
// Return transaction ID
96+
c.String(202, txIdHex)
97+
}

cmd/tx-submit-api-mirror/main.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"github.com/cloudstruct/tx-submit-api-mirror/api"
7+
"github.com/cloudstruct/tx-submit-api-mirror/config"
8+
"github.com/cloudstruct/tx-submit-api-mirror/logging"
9+
"os"
10+
)
11+
12+
var cmdlineFlags struct {
13+
configFile string
14+
}
15+
16+
func main() {
17+
flag.StringVar(&cmdlineFlags.configFile, "config", "", "path to config file to load")
18+
flag.Parse()
19+
20+
// Load config
21+
cfg, err := config.Load(cmdlineFlags.configFile)
22+
if err != nil {
23+
fmt.Printf("Failed to load config: %s\n", err)
24+
os.Exit(1)
25+
}
26+
27+
// Configure logging
28+
logging.Setup(&cfg.Logging)
29+
logger := logging.GetLogger()
30+
// Sync logger on exit
31+
defer func() {
32+
if err := logger.Sync(); err != nil {
33+
// We don't actually care about the error here, but we have to do something
34+
// to appease the linter
35+
return
36+
}
37+
}()
38+
39+
// Start API listener
40+
logger.Infof("starting API listener on %s:%d", cfg.Api.ListenAddress, cfg.Api.ListenPort)
41+
if err := api.Start(cfg); err != nil {
42+
logger.Fatalf("failed to start API: %s", err)
43+
}
44+
45+
// Wait forever
46+
select {}
47+
}

config/config.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"github.com/kelseyhightower/envconfig"
6+
"gopkg.in/yaml.v2"
7+
"io/ioutil"
8+
)
9+
10+
type Config struct {
11+
Logging LoggingConfig `yaml:"logging"`
12+
Api ApiConfig `yaml:"api"`
13+
Backends []string `yaml:"backends" envconfig:"BACKENDS"`
14+
}
15+
16+
type LoggingConfig struct {
17+
Level string `yaml:"level" envconfig:"LOGGING_LEVEL"`
18+
}
19+
20+
type ApiConfig struct {
21+
ListenAddress string `yaml:"address" envconfig:"API_LISTEN_ADDRESS"`
22+
ListenPort uint `yaml:"port" envconfig:"API_LISTEN_PORT"`
23+
}
24+
25+
// Singleton config instance with default values
26+
var globalConfig = &Config{
27+
Logging: LoggingConfig{
28+
Level: "info",
29+
},
30+
Api: ApiConfig{
31+
ListenAddress: "",
32+
ListenPort: 8090,
33+
},
34+
}
35+
36+
func Load(configFile string) (*Config, error) {
37+
// Load config file as YAML if provided
38+
if configFile != "" {
39+
buf, err := ioutil.ReadFile(configFile)
40+
if err != nil {
41+
return nil, fmt.Errorf("error reading config file: %s", err)
42+
}
43+
err = yaml.Unmarshal(buf, globalConfig)
44+
if err != nil {
45+
return nil, fmt.Errorf("error parsing config file: %s", err)
46+
}
47+
}
48+
// Load config values from environment variables
49+
// We use "dummy" as the app name here to (mostly) prevent picking up env
50+
// vars that we hadn't explicitly specified in annotations above
51+
err := envconfig.Process("dummy", globalConfig)
52+
if err != nil {
53+
return nil, fmt.Errorf("error processing environment: %s", err)
54+
}
55+
return globalConfig, nil
56+
}
57+
58+
// Return global config instance
59+
func GetConfig() *Config {
60+
return globalConfig
61+
}

go.mod

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module github.com/cloudstruct/tx-submit-api-mirror
2+
3+
go 1.17
4+
5+
require (
6+
github.com/fxamacker/cbor/v2 v2.4.0
7+
github.com/gin-contrib/zap v0.0.2
8+
github.com/gin-gonic/gin v1.7.7
9+
github.com/kelseyhightower/envconfig v1.4.0
10+
go.uber.org/zap v1.21.0
11+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
12+
gopkg.in/yaml.v2 v2.4.0
13+
)
14+
15+
require (
16+
github.com/gin-contrib/sse v0.1.0 // indirect
17+
github.com/go-playground/locales v0.13.0 // indirect
18+
github.com/go-playground/universal-translator v0.17.0 // indirect
19+
github.com/go-playground/validator/v10 v10.4.1 // indirect
20+
github.com/golang/protobuf v1.3.3 // indirect
21+
github.com/json-iterator/go v1.1.9 // indirect
22+
github.com/leodido/go-urn v1.2.0 // indirect
23+
github.com/mattn/go-isatty v0.0.12 // indirect
24+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
25+
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
26+
github.com/ugorji/go/codec v1.1.7 // indirect
27+
github.com/x448/float16 v0.8.4 // indirect
28+
go.uber.org/atomic v1.7.0 // indirect
29+
go.uber.org/multierr v1.6.0 // indirect
30+
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
31+
)

0 commit comments

Comments
 (0)