diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..ca91146 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,45 @@ +# This file was copied from the following URL and modified: +# https://github.com/golangci/golangci-lint-action/blob/master/README.md#how-to-use + +name: golangci-lint +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + # pull-requests: read +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v3 + with: + go-version: '1.17' + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: v1.45.2 # current version at time of commit + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + # args: --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true diff --git a/.gitignore b/.gitignore index 66fd13c..3145127 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +# Binary +/cardano-submit-api diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8208832 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.17 AS build + +WORKDIR /code +COPY . . +RUN make build + +FROM ubuntu:focal AS final +COPY --from=build /code/cardano-submit-api /usr/local/bin/ +ENTRYPOINT ["/usr/local/bin/cardano-submit-api"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fa77b62 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +BINARY=cardano-submit-api + +# Determine root directory +ROOT_DIR=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) + +# Gather all .go files for use in dependencies below +GO_FILES=$(shell find $(ROOT_DIR) -name '*.go') + +# Build our program binary +# Depends on GO_FILES to determine when rebuild is needed +$(BINARY): $(GO_FILES) + # Needed to fetch new dependencies and add them to go.mod + go mod tidy + go build -o $(BINARY) ./cmd/$(BINARY) + +.PHONY: build image + +# Alias for building program binary +build: $(BINARY) + +# Build docker image +image: build + docker build -t cloudstruct/$(BINARY) . diff --git a/cmd/cardano-submit-api/main.go b/cmd/cardano-submit-api/main.go new file mode 100644 index 0000000..5ac5c58 --- /dev/null +++ b/cmd/cardano-submit-api/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "flag" + "fmt" + "github.com/cloudstruct/go-cardano-submit-api/internal/api" + "github.com/cloudstruct/go-cardano-submit-api/internal/config" + "github.com/cloudstruct/go-cardano-submit-api/internal/logging" + "os" +) + +var cmdlineFlags struct { + configFile string +} + +func main() { + flag.StringVar(&cmdlineFlags.configFile, "config", "", "path to config file to load") + flag.Parse() + + // Load config + cfg, err := config.Load(cmdlineFlags.configFile) + if err != nil { + fmt.Printf("Failed to load config: %s\n", err) + os.Exit(1) + } + + // Configure logging + logging.Setup(&cfg.Logging) + logger := logging.GetLogger() + // Sync logger on exit + defer func() { + if err := logger.Sync(); err != nil { + // We don't actually care about the error here, but we have to do something + // to appease the linter + return + } + }() + + // Start API listener + logger.Infof("starting API listener on %s:%d", cfg.Api.ListenAddress, cfg.Api.ListenPort) + if err := api.Start(cfg); err != nil { + logger.Fatalf("failed to start API: %s", err) + } + + // Wait forever + select {} +} diff --git a/config.yaml.example b/config.yaml.example new file mode 100644 index 0000000..a6befd7 --- /dev/null +++ b/config.yaml.example @@ -0,0 +1,35 @@ +--- +# Example config file for cardano-submit-api +# The values shown below correspond to the in-code defaults + +logging: + # Logging level + # + # This can also be set via the LOGGING_LEVEL environment variable + level: info + +api: + # Listen address for the API + # + # This can also be set via the API_LISTEN_ADDRESS environment variable + address: + + # Listen port for the API + # + # This can also be set via the API_LISTEN_PORT environment variable + port: 8090 + +node: + # Path to UNIX socket file for cardano-node + # + # This can also be set via the NODE_SOCKET_PATH environment variable + socketPath: + + # Address/port for cardano-node + # + # This requires that you be running socat or similar to create a bridge + # between TCP and the UNIX socket. + # + # These can also be set via the NODE_ADDRESS and NODE_PORT environment variables + address: + port: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5c75ca7 --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module github.com/cloudstruct/go-cardano-submit-api + +go 1.17 + +require ( + github.com/cloudstruct/go-ouroboros-network v0.13.0 + github.com/fxamacker/cbor/v2 v2.4.0 + github.com/gin-contrib/zap v0.0.2 + github.com/gin-gonic/gin v1.7.7 + github.com/kelseyhightower/envconfig v1.4.0 + go.uber.org/zap v1.21.0 + golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/golang/protobuf v1.3.3 // indirect + github.com/json-iterator/go v1.1.9 // indirect + github.com/leodido/go-urn v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect + github.com/ugorji/go/codec v1.1.7 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d1a1e0d --- /dev/null +++ b/go.sum @@ -0,0 +1,114 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/cloudstruct/go-ouroboros-network v0.13.0 h1:q6l4QQocuDTz0TOynEo5jwe9R+9tCL6vLJpWdMer9kc= +github.com/cloudstruct/go-ouroboros-network v0.13.0/go.mod h1:06ggUMerJLwKJkVi5DLX1QMDz8r+R8o92rht7Z4ss9k= +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/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= +github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-contrib/zap v0.0.2 h1:VnIucI+kUsxgzmcrX0gMk19a2I12KirTxi+ufuT2xZk= +github.com/gin-contrib/zap v0.0.2/go.mod h1:2vZj8gTuOYOfottCirxZr9gNM/Q1yk2iSVn15SUVG5A= +github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +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-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +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.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +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-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE= +golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000..8d1249c --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,126 @@ +package api + +import ( + "encoding/hex" + "fmt" + "github.com/cloudstruct/go-cardano-submit-api/internal/config" + "github.com/cloudstruct/go-cardano-submit-api/internal/logging" + ouroboros "github.com/cloudstruct/go-ouroboros-network" + "github.com/cloudstruct/go-ouroboros-network/block" + "github.com/cloudstruct/go-ouroboros-network/protocol/localtxsubmission" + "github.com/fxamacker/cbor/v2" + ginzap "github.com/gin-contrib/zap" + "github.com/gin-gonic/gin" + "golang.org/x/crypto/blake2b" + "io/ioutil" +) + +func Start(cfg *config.Config) error { + // Disable gin debug output + gin.SetMode(gin.ReleaseMode) + gin.DisableConsoleColor() + + // Configure router + router := gin.New() + // Catch panics and return a 500 + router.Use(gin.Recovery()) + // Access logging + accessLogger := logging.GetAccessLogger() + router.Use(ginzap.Ginzap(accessLogger, "", true)) + router.Use(ginzap.RecoveryWithZap(accessLogger, true)) + + // Configure routes + router.GET("/healthcheck", handleHealthcheck) + router.POST("/api/submit/tx", handleSubmitTx) + + // Start listener + err := router.Run(fmt.Sprintf("%s:%d", cfg.Api.ListenAddress, cfg.Api.ListenPort)) + return err +} + +func handleHealthcheck(c *gin.Context) { + // TODO: add some actual health checking here + c.JSON(200, gin.H{"failed": false}) +} + +func handleSubmitTx(c *gin.Context) { + cfg := config.GetConfig() + logger := logging.GetLogger() + // Read transaction from request body + rawTx, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + logger.Errorf("failed to read request body: %s", err) + c.String(500, "failed to request body") + return + } + if err := c.Request.Body.Close(); err != nil { + logger.Errorf("failed to close request body: %s", err) + } + // Unwrap transaction and calculate ID + var txUnwrap []cbor.RawMessage + if err := cbor.Unmarshal(rawTx, &txUnwrap); err != nil { + logger.Errorf("failed to unwrap transaction CBOR: %s", err) + c.String(400, fmt.Sprintf("failed to unwrap transaction CBOR: %s", err)) + return + } + txId := blake2b.Sum256(txUnwrap[0]) + txIdHex := hex.EncodeToString(txId[:]) + // Connect to cardano-node and submit TX + errorChan := make(chan error) + doneChan := make(chan bool) + oOpts := &ouroboros.OuroborosOptions{ + NetworkMagic: uint32(cfg.Node.NetworkMagic), + ErrorChan: errorChan, + UseNodeToNodeProtocol: false, + LocalTxSubmissionCallbackConfig: &localtxsubmission.CallbackConfig{ + AcceptTxFunc: func() error { + // Return transaction ID + c.String(202, txIdHex) + doneChan <- true + return nil + }, + RejectTxFunc: func(reason interface{}) error { + c.String(400, fmt.Sprintf("transaction rejected by node: %#v", reason)) + doneChan <- true + return nil + }, + }, + } + go func() { + err, ok := <-errorChan + if ok { + logger.Errorf("failure communicating with node: %s", err) + c.String(500, "failure communicating with node") + doneChan <- true + } + }() + oConn, err := ouroboros.New(oOpts) + if err != nil { + logger.Errorf("failure creating Ouroboros connection: %s", err) + c.String(500, "failure communicating with node") + return + } + if cfg.Node.Address != "" && cfg.Node.Port > 0 { + if err := oConn.Dial("tcp", fmt.Sprintf("%s:%d", cfg.Node.Address, cfg.Node.Port)); err != nil { + logger.Errorf("failure connecting to node via TCP: %s", err) + c.String(500, "failure communicating with node") + return + } + } else { + if err := oConn.Dial("unix", cfg.Node.SocketPath); err != nil { + logger.Errorf("failure connecting to node via UNIX socket: %s", err) + c.String(500, "failure communicating with node") + return + } + } + // TODO: figure out better way to determine era + if err = oConn.LocalTxSubmission.SubmitTx(block.TX_TYPE_ALONZO, rawTx); err != nil { + logger.Errorf("failure submitting transaction: %s", err) + c.String(500, "failure communicating with node") + return + } + // Wait for async process to finish + <-doneChan + // We have to close the channel to break out of the Goroutine waiting on it + close(errorChan) +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..8c2529c --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,117 @@ +package config + +import ( + "fmt" + "github.com/kelseyhightower/envconfig" + "gopkg.in/yaml.v2" + "io/ioutil" + "os" +) + +const ( + TESTNET_MAGIC = 1097911063 + MAINNET_MAGIC = 764824073 +) + +type Config struct { + Logging LoggingConfig `yaml:"logging"` + Api ApiConfig `yaml:"api"` + Node NodeConfig `yaml:"node"` +} + +type LoggingConfig struct { + Level string `yaml:"level" envconfig:"LOGGING_LEVEL"` +} + +type ApiConfig struct { + ListenAddress string `yaml:"address" envconfig:"API_LISTEN_ADDRESS"` + ListenPort uint `yaml:"port" envconfig:"API_LISTEN_PORT"` +} + +type NodeConfig struct { + Network string `yaml:"network" envconfig:"NETWORK"` + NetworkMagic uint32 `yaml:"networkMagic" envconfig:"CARDANO_NODE_NETWORK_MAGIC"` + Address string `yaml:"address" envconfig:"CARDANO_NODE_ADDRESS"` + Port uint `yaml:"port" envconfig:"CARDANO_NODE_PORT"` + SocketPath string `yaml:"socketPath" envconfig:"CARDANO_NODE_SOCKET_PATH"` +} + +// Singleton config instance with default values +var globalConfig = &Config{ + Logging: LoggingConfig{ + Level: "info", + }, + Api: ApiConfig{ + ListenAddress: "", + ListenPort: 8090, + }, + Node: NodeConfig{ + Network: "mainnet", + SocketPath: "/node-ipc/node.socket", + }, +} + +func Load(configFile string) (*Config, error) { + // Load config file as YAML if provided + if configFile != "" { + buf, err := ioutil.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("error reading config file: %s", err) + } + err = yaml.Unmarshal(buf, globalConfig) + if err != nil { + return nil, fmt.Errorf("error parsing config file: %s", err) + } + } + // Load config values from environment variables + // We use "dummy" as the app name here to (mostly) prevent picking up env + // vars that we hadn't explicitly specified in annotations above + err := envconfig.Process("dummy", globalConfig) + if err != nil { + return nil, fmt.Errorf("error processing environment: %s", err) + } + if err := globalConfig.populateNetworkMagic(); err != nil { + return nil, err + } + if err := globalConfig.checkNode(); err != nil { + return nil, err + } + return globalConfig, nil +} + +// Return global config instance +func GetConfig() *Config { + return globalConfig +} + +func (c *Config) populateNetworkMagic() error { + if c.Node.Network != "" { + switch c.Node.Network { + case "testnet": + c.Node.NetworkMagic = TESTNET_MAGIC + case "mainnet": + c.Node.NetworkMagic = MAINNET_MAGIC + default: + return fmt.Errorf("unknown network: %s", c.Node.Network) + } + } + return nil +} + +func (c *Config) checkNode() error { + if c.Node.Address != "" && c.Node.Port > 0 { + // TODO: add some validation for node address/port + } else if c.Node.SocketPath != "" { + // Check that node socket path exists + if _, err := os.Stat(c.Node.SocketPath); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("node socket path does not exist: %s", c.Node.SocketPath) + } else { + return fmt.Errorf("unknown error checking if node socket path exists: %s", err) + } + } + } else { + return fmt.Errorf("you must specify either the UNIX socket path or the address/port for your cardano-node") + } + return nil +} diff --git a/internal/logging/logging.go b/internal/logging/logging.go new file mode 100644 index 0000000..df34a41 --- /dev/null +++ b/internal/logging/logging.go @@ -0,0 +1,52 @@ +package logging + +import ( + "github.com/cloudstruct/go-cardano-submit-api/internal/config" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "log" + "time" +) + +type Logger = zap.SugaredLogger + +var globalLogger *Logger + +func Setup(cfg *config.LoggingConfig) { + // Build our custom logging config + loggerConfig := zap.NewProductionConfig() + // Change timestamp key name + loggerConfig.EncoderConfig.TimeKey = "timestamp" + // Use a human readable time format + loggerConfig.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.RFC3339) + + // Set level + if cfg.Level != "" { + level, err := zapcore.ParseLevel(cfg.Level) + if err != nil { + log.Fatalf("error configuring logger: %s", err) + } + loggerConfig.Level.SetLevel(level) + } + + // Create the logger + l, err := loggerConfig.Build() + if err != nil { + log.Fatal(err) + } + + // Store the "sugared" version of the logger + globalLogger = l.Sugar() +} + +func GetLogger() *zap.SugaredLogger { + return globalLogger +} + +func GetDesugaredLogger() *zap.Logger { + return globalLogger.Desugar() +} + +func GetAccessLogger() *zap.Logger { + return globalLogger.Desugar().With(zap.String("type", "access")).WithOptions(zap.WithCaller(false)) +}