Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
16 changes: 0 additions & 16 deletions .env.example

This file was deleted.

3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# do not track env vars
.env
.env.*

# ignore generated coverage file
coverage.txt

# do not track binary
vote-collector
/vote-collector
1 change: 1 addition & 0 deletions .ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
vendor/
74 changes: 55 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
> Simple HTTP vote collection API service in Go

## Table of Contents

- [Install](#install)
- [Dependencies](#dependencies)
- [Usage](#usage)
Expand All @@ -16,14 +17,48 @@
- [Contributing](#contributing)
- [License](#license)

## API

- POST `/api/vote`
```json
{
"addr": "XyBmeuLa8y3D3XmzPvCTj5PVh7WvMPkLn1",
"msg": "dte2022-afrancis|ctafti",
"sig": "IIm+2++GxT4OtTTY4aZK0iKIWh21yxiwomfY76l197qtVB42KVpy53QxS65zq1R9eN2XLcGh2YsedsVtsmrw2OE="
}
```
- GET `/api/votes`
- GET `/api/all-votes`
- GET `/api/candidates`
```json
[
{
"name": "John Doe, III",
"handle": "@johndoe",
"email": "john.doe@example.com"
}
]
```

## Install

Clone the repo (or install via `go get`) and build the project. A makefile has been included for convenience.
Clone the repo and build the project.

```sh
git clone https://github.com/dashevo/vote-collector.git
cd vote-collector
make
pushd ./vote-collector/
```

### Pre-Reqs

Install `go` and `dotenv`:

```sh
# dotenv - for running with .env config
curl https://webinstall.dev/dotenv | bash

# Go + standard tooling and "x" tools
curl https://webinstall.dev/go | bash
```

### Dependencies
Expand All @@ -32,64 +67,65 @@ The vote collector simply logs votes to a Postgres database, therefore a running

## Usage

First, copy `.env.example` to `.env` and modify accordingly. Postgres variables need to be configured to point to an accessible, running Postgres instance.
First, copy `example.env` to `.env` and modify accordingly. Postgres variables need to be configured to point to an accessible, running Postgres instance.

```sh
# config
cp .env.example .env
cp example.env .env
vi .env # (edit accordingly)

# run
go run vote-collector
go run -mod=vendor vote-collector

# -or-
go build
./vote-collector
go build -mod=vendor
dotenv ./vote-collector
```

## Configuration

The vote collector uses environment variables for configuration. Variables are read from a `.env` file and can be overwritten by variables defined in the environment or directly passed to the process. See all available settings in [.env.example](.env.example).
The vote collector uses environment variables for configuration. Variables are read from a `.env` file and can be overwritten by variables defined in the environment or directly passed to the process. See all available settings in [example.env](example.env).

### Quick start

A `docker-compose` file is included for testing purposes, which also sets up a Postgres database.

```
cp .env.example .env
```sh
cp example.env .env
vi .env # (edit accordingly)

docker-compose up
```

To verify:

```
curl -i http://127.0.0.1:7001/health
```sh
curl -i http://127.0.0.1:7001/api/health
```

### Generating a JWT

Some routes in the API are only available with authentication. These are the audit routes, which allow reading vote entries:

* `/allVotes`
* `/validVotes`
- GET `/api/allVotes`
- GET `/api/validVotes`

For these, a JWT token must be sent in the header (see `curl_examples.sh` in this repo). There is currently no authentication table or route, so this must be manually generated.

To generate the JWT token, you can use the [JWT Debugger](https://jwt.io/#debugger-io). Simply visit the site, adjust the payload data accordingly, and in place of `your-256-bit-secret`, use the value of `$JWT_SECRET_KEY` that you set in the .env file (can be any secret string). Then click the "Share JWT" button to retrieve the JWT. This is the value you should send in the header after "Authorization: Bearer ".

An example JWT token looks like:

```
```jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJUZXN0IFRlc3RlcnNvbiIsInN1YiI6IkpvaG4gRG9udXQiLCJpYXQiOjE1NTE0NjYyMjN9.Z03u0ZogZZ4W2C9E7FgisQxWqp-XsnuS48JAxzRxQ1I
```

*Note that this is just an example and will not work with any production deployment.*
_Note that this is just an example and will not work with any production deployment._

## Maintainer
## Maintainers

[@nmarley](https://github.com/nmarley)
- 2022 [@coolaj86](https://github.com/coolaj86)
- 2019 [@nmarley](https://github.com/nmarley)

## Contributing

Expand Down
74 changes: 70 additions & 4 deletions collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,38 @@ package main

import (
"fmt"
"log"
"net/http"
"os"
"sync"
"time"

"github.com/go-pg/pg"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"

"github.com/joho/godotenv"
)

// server is an object which implements the http.Handler interface (passes to
// router) and related connection objects hang off it (e.g. db conn)
type server struct {
router mux.Router
db *pg.DB
router mux.Router
db *pg.DB
gsheetKey string
mnlistURL string
candidatesUpdateMux *sync.Mutex
candidatesMux *sync.RWMutex
candidates []Candidate
votingAddresses []string
candidatesUpdatedAt time.Time
votingStart time.Time
votingEnd time.Time
}

// MNInfo represents the voting keys of the nodes
type MNInfo struct {
VotingAddress string `json:"votingaddress"`
}

// envCheck is called upon startup to ensure the required environment variables
Expand All @@ -27,6 +46,10 @@ func envCheck() {
"PGPORT",
"PGPASSWORD",
"PGDATABASE",
"VOTING_START_DATE",
"VOTING_END_DATE",
"GSHEET_KEY",
"MNLIST_URL",
"JWT_SECRET_KEY",
"DASH_NETWORK",
"BIND_HOST",
Expand All @@ -52,6 +75,9 @@ func envCheck() {
}

func main() {
_ = godotenv.Load(".env")
_ = godotenv.Load(".env.secret")

envCheck()

// create a PG database connection
Expand All @@ -70,10 +96,48 @@ func main() {
os.Exit(1)
}

votingStart, err := time.Parse(time.RFC3339, os.Getenv("VOTING_START_DATE"))
if nil != err {
fmt.Fprintf(
os.Stderr,
"error parsing 'VOTING_START_DATE': %q: %s\n",
os.Getenv("VOTING_START_DATE"),
err,
)
os.Exit(1)
}
votingEnd, err := time.Parse(time.RFC3339, os.Getenv("VOTING_END_DATE"))
if nil != err {
fmt.Fprintf(
os.Stderr,
"error parsing 'VOTING_END_DATE': %q: %s\n",
os.Getenv("VOTING_END_DATE"),
err,
)
os.Exit(1)
}

// create a server object and add db connection
srv := server{
db: db,
db: db,
gsheetKey: os.Getenv("GSHEET_KEY"),
mnlistURL: os.Getenv("MNLIST_URL"),
votingStart: votingStart,
votingEnd: votingEnd,
candidatesMux: &sync.RWMutex{},
candidatesUpdateMux: &sync.Mutex{},
}

err = srv.updateLists()
if nil != err {
fmt.Fprintf(os.Stderr, "error parsing candidates from CSV using 'GSHEET_KEY': %s\n", err)
os.Exit(1)
}
if 0 == len(srv.votingAddresses) {
fmt.Fprintf(os.Stderr, "error getting voting addresses from URL using 'MNLIST_URL': %s\n", srv.mnlistURL)
os.Exit(1)
}

srv.routes()

// allow CORS w/mux router
Expand All @@ -84,5 +148,7 @@ func main() {
// serve the API
listenAt := os.Getenv("BIND_HOST") + ":" + os.Getenv("BIND_PORT")
fmt.Printf("%s listening at %s\n", os.Args[:1], listenAt)
http.ListenAndServe(listenAt, handlers.CORS(originsOk, headersOk, methodsOk)(srv))
if err := http.ListenAndServe(listenAt, handlers.CORS(originsOk, headersOk, methodsOk)(srv)); nil != err {
log.Fatal(err)
}
}
26 changes: 26 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Postgres connection info
export PGHOST="localhost"
export PGPORT="5432"
export PGDATABASE="postgres"
export PGUSER="postgres"
export PGPASSWORD="postgres"

export GSHEET_KEY="1AOg39MP4hiyvNJT7raoK1OKy4FbnQKCQbwJzqYaBRKI"

# secret key for verifying signed JWT tokens
export JWT_SECRET_KEY="supersecretkeyhere"

# voting is only allowed between the start and end date
# (untimely requests will be rejected)
export VOTING_START_DATE='2022-04-03T00:00:00Z'
export VOTING_END_DATE='2022-04-15T00:00:00Z'

# Dash network (mainnet or testnet)
export DASH_NETWORK="testnet"

# Where to fetch the mnlist.json
export MNLIST_URL="https://dashnode.duckdns.org/api/mnlist"

# What network / port should the API service bind to
export BIND_HOST="127.0.0.1"
export BIND_PORT="7001"
19 changes: 16 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
module github.com/dashevo/vote-collector

go 1.17

require (
github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
github.com/dashhive/dashmsg v0.10.4
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/go-pg/pg v7.1.7+incompatible
github.com/gorilla/handlers v1.4.0
github.com/gorilla/mux v1.7.0
github.com/gorilla/mux v1.8.0
github.com/joho/godotenv v1.4.0
)

require (
github.com/anaskhan96/base58check v0.0.0-20181220122047-b05365d494c4 // indirect
github.com/btcsuite/btcd v0.20.1-beta // indirect
github.com/ethereum/go-ethereum v1.10.16 // indirect
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.18.1 // indirect
golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
mellium.im/sasl v0.2.1 // indirect
)
Loading