├── .data └── postgres │ └── .gitkeep ├── logo_green.png ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── bundle.sh ├── go.work ├── scripts ├── setup_test_db.sh ├── reset_db.sh ├── cf_ips.sh └── debug.sh ├── kubernetes ├── moneybags │ ├── kustomization.yaml │ ├── cron.yaml │ └── rpc_cron.yaml ├── kustomization.yaml ├── svc.yaml ├── ingress.yaml └── deployment.yaml ├── libs ├── utils │ ├── validation │ │ ├── email.go │ │ ├── email_test.go │ │ ├── password.go │ │ ├── work_test.go │ │ ├── banano_test.go │ │ ├── work.go │ │ ├── password_test.go │ │ └── banano.go │ ├── misc │ │ ├── slice.go │ │ └── slice_test.go │ ├── format │ │ ├── time.go │ │ └── time_test.go │ ├── auth │ │ ├── crypt_test.go │ │ ├── crypt.go │ │ ├── jwt_test.go │ │ └── jwt.go │ ├── net │ │ ├── ip_test.go │ │ ├── headers.go │ │ ├── headers_test.go │ │ └── ip.go │ ├── go.mod │ ├── number │ │ ├── banano.go │ │ └── banano_test.go │ ├── env_test.go │ ├── env.go │ ├── go.sum │ ├── testing │ │ └── main.go │ └── ed25519 │ │ ├── ed25519_test.go │ │ └── ed25519.go └── models │ ├── work_response.go │ ├── go.mod │ ├── go.sum │ ├── work_response_test.go │ ├── client_message.go │ ├── nano.go │ └── client_message_test.go ├── apps ├── client │ ├── linux_macos │ │ └── README.md │ ├── windows │ │ ├── README.md │ │ └── run-with-options.bat │ ├── genqlient.graphql │ ├── genqlient.yaml │ ├── Dockerfile.build │ ├── work │ │ ├── benchmark.go │ │ ├── nanowork.go │ │ └── processor.go │ ├── go.mod │ ├── models │ │ ├── random_access_queue_test.go │ │ └── random_access_queue.go │ ├── README.md │ ├── gql │ │ ├── client.go │ │ └── generated.go │ └── websocket │ │ └── client.go └── server │ ├── src │ ├── config │ │ └── main.go │ ├── email │ │ ├── models.go │ │ ├── templates │ │ │ ├── base.html │ │ │ ├── serviceapproved.html │ │ │ └── resetpassword.html │ │ └── mailer.go │ ├── models │ │ ├── stats_singleton.go │ │ ├── payment.go │ │ ├── types.go │ │ ├── work_result.go │ │ ├── base.go │ │ ├── user.go │ │ ├── sync_array_test.go │ │ └── sync_array.go │ ├── repository │ │ ├── stats_repo.go │ │ └── payment_repo.go │ ├── database │ │ ├── postgres.go │ │ └── redis_test.go │ ├── tests │ │ ├── payment_repo_test.go │ │ ├── user_repo_test.go │ │ ├── work_repo_test.go │ │ └── auth_test.go │ ├── net │ │ └── nanows_client.go │ ├── controller │ │ └── worker_ws.go │ └── middleware │ │ └── auth.go │ ├── graph │ ├── resolver.go │ ├── schema.graphqls │ └── model │ │ └── models_gen.go │ ├── Dockerfile │ ├── README.md │ ├── gqlgen.yml │ └── go.mod ├── .gitignore ├── services └── moneybags │ ├── Dockerfile │ ├── go.mod │ ├── rpcClient.go │ └── main.go ├── Dockerfile.dev ├── LICENSE ├── docker-compose.yaml ├── logo.svg ├── logo_green.svg ├── README.md ├── .github └── workflows │ └── ci.yml └── go.work.sum /.data/postgres/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logo_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BananoCoin/boompow/HEAD/logo_green.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "gopls": { 3 | "experimentalWorkspaceModule": true 4 | } 5 | } -------------------------------------------------------------------------------- /bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | zip -r9 linux_amd64.zip ./apps/client/target/boompow-client ./apps/client/linux_macos/README.md -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.19 2 | 3 | use ( 4 | ./libs/models 5 | ./libs/utils 6 | ./apps/client 7 | ./apps/server 8 | ./services/moneybags 9 | ) 10 | -------------------------------------------------------------------------------- /scripts/setup_test_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL 5 | CREATE DATABASE testing; 6 | EOSQL -------------------------------------------------------------------------------- /kubernetes/moneybags/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: boompow-next 4 | resources: 5 | - cron.yaml 6 | - rpc_cron.yaml -------------------------------------------------------------------------------- /kubernetes/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | namespace: boompow-next 4 | resources: 5 | - deployment.yaml 6 | - svc.yaml 7 | - ingress.yaml -------------------------------------------------------------------------------- /libs/utils/validation/email.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import "net/mail" 4 | 5 | func IsValidEmail(email string) bool { 6 | _, err := mail.ParseAddress(email) 7 | return err == nil 8 | } 9 | -------------------------------------------------------------------------------- /libs/utils/misc/slice.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | func Contains[T comparable](elems []T, v T) bool { 4 | for _, s := range elems { 5 | if v == s { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /apps/client/linux_macos/README.md: -------------------------------------------------------------------------------- 1 | # BoomPoW Client 2 | 3 | To use the client, simply run it with 4 | 5 | ``` 6 | ./boompow-client 7 | ``` 8 | 9 | To see options: 10 | 11 | ``` 12 | ./boompow-client -help 13 | ``` 14 | -------------------------------------------------------------------------------- /apps/client/windows/README.md: -------------------------------------------------------------------------------- 1 | # BoomPoW Client 2 | 3 | To use the client, simply run `boompow-client.exe` 4 | 5 | To edit default options, edit the `run-with-options.bat` script with your desired options and run that instead. 6 | -------------------------------------------------------------------------------- /apps/client/genqlient.graphql: -------------------------------------------------------------------------------- 1 | mutation loginUser($input: LoginInput!) { 2 | login(input: $input) { 3 | token 4 | } 5 | } 6 | 7 | mutation refreshToken($input: RefreshTokenInput!) { 8 | refreshToken(input: $input) 9 | } 10 | -------------------------------------------------------------------------------- /libs/models/work_response.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Work response sent from client -> server 4 | type ClientWorkResponse struct { 5 | RequestID string `json:"request_id"` 6 | Hash string `json:"hash"` 7 | Result string `json:"result"` 8 | } 9 | -------------------------------------------------------------------------------- /kubernetes/svc.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: boompow-service 5 | namespace: boompow-next 6 | spec: 7 | selector: 8 | app: boompow 9 | type: ClusterIP 10 | ports: 11 | - port: 8080 12 | targetPort: 8080 -------------------------------------------------------------------------------- /apps/client/genqlient.yaml: -------------------------------------------------------------------------------- 1 | # Default genqlient config; for full documentation see: 2 | # https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml 3 | schema: ../server/graph/schema.graphqls 4 | operations: 5 | - genqlient.graphql 6 | generated: ./gql/generated.go 7 | -------------------------------------------------------------------------------- /libs/utils/format/time.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import "time" 4 | 5 | // GenerateISOString generates a time string equivalent to Date.now().toISOString in JavaScript 6 | func GenerateISOString(dt time.Time) string { 7 | return dt.Format("2006-01-02T15:04:05.999Z07:00") 8 | } 9 | -------------------------------------------------------------------------------- /apps/server/src/config/main.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // How long the 6-digit confirmation code is good for 4 | const EMAIL_CONFIRMATION_TOKEN_VALID_MINUTES = 180 5 | 6 | // The nano send difficulty multiplier is x64, receive is x1 (banano is x1) 7 | const MAX_WORK_DIFFICULTY_MULTIPLIER = 64 8 | -------------------------------------------------------------------------------- /apps/client/Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM golang:1.19-buster 2 | 3 | WORKDIR /src 4 | 5 | # Install useful tools 6 | RUN apt-get update && apt-get install -y build-essential ocl-icd-opencl-dev git && \ 7 | rm -rf /var/lib/apt/lists/* 8 | 9 | 10 | ADD . . 11 | 12 | CMD ["./build.sh"] 13 | 14 | -------------------------------------------------------------------------------- /libs/models/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bananocoin/boompow/libs/models 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/bananocoin/boompow/libs/utils v0.0.0-20220810021633-b4ba8d652a46 7 | github.com/google/uuid v1.3.0 8 | ) 9 | 10 | require github.com/golang/glog v1.0.0 // indirect 11 | -------------------------------------------------------------------------------- /scripts/reset_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ### Resets the database without applying migrations 3 | 4 | if [[ -z $DATABASE_URL ]]; then 5 | echo "DATABASE_URL not set" 6 | exit 1 7 | fi 8 | 9 | 10 | psql $DATABASE_URL << EOF 11 | drop schema public cascade; 12 | create schema public; 13 | EOF -------------------------------------------------------------------------------- /libs/utils/validation/email_test.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "testing" 5 | 6 | utils "github.com/bananocoin/boompow/libs/utils/testing" 7 | ) 8 | 9 | func TestEmailValidation(t *testing.T) { 10 | utils.AssertEqual(t, true, IsValidEmail("helloworld@gmail.com")) 11 | utils.AssertEqual(t, false, IsValidEmail("helloworldgmail.com")) 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Launch containers", 6 | "type": "shell", 7 | "command": "docker-compose up -d", 8 | "group": "none", 9 | "presentation": { 10 | "reveal": "always", 11 | "panel": "new" 12 | }, 13 | "runOptions": { 14 | "runOn": "folderOpen" 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /libs/utils/format/time_test.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | utils "github.com/bananocoin/boompow/libs/utils/testing" 8 | ) 9 | 10 | func TestFormatTime(t *testing.T) { 11 | time := time.Unix(1659715327, 0) 12 | formatted := GenerateISOString(time.UTC()) 13 | 14 | utils.AssertEqual(t, "2022-08-05T16:02:07Z", formatted) 15 | } 16 | -------------------------------------------------------------------------------- /scripts/cf_ips.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cf_ips() { 6 | echo "# https://www.cloudflare.com/ips" 7 | 8 | for type in v4 v6; do 9 | echo "# IP$type" 10 | curl -sL "https://www.cloudflare.com/ips-$type/" | sed "s|^|allow |g" | sed "s|\$|;|g" 11 | echo 12 | done 13 | 14 | echo "# Generated at $(LC_ALL=C date)" 15 | } 16 | 17 | (cf_ips && echo "deny all; # deny all remaining ips") 18 | -------------------------------------------------------------------------------- /libs/utils/auth/crypt_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "testing" 5 | 6 | utils "github.com/bananocoin/boompow/libs/utils/testing" 7 | ) 8 | 9 | func TestHashPassword(t *testing.T) { 10 | password := "password" 11 | hash, _ := HashPassword(password) 12 | utils.AssertEqual(t, true, CheckPasswordHash(password, hash)) 13 | utils.AssertEqual(t, false, CheckPasswordHash("password1", hash)) 14 | } 15 | -------------------------------------------------------------------------------- /libs/utils/net/ip_test.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "testing" 5 | 6 | utils "github.com/bananocoin/boompow/libs/utils/testing" 7 | ) 8 | 9 | func TestIsIPInRange(t *testing.T) { 10 | ip := "123.45.67.89" 11 | 12 | utils.AssertEqual(t, false, IsIPInHetznerRange(ip)) 13 | utils.AssertEqual(t, true, IsIPInHetznerRange("95.216.77.23")) 14 | utils.AssertEqual(t, true, IsIPInHetznerRange("2a01:4f9:c010:780c::1")) 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // Run in container: 2 | // dlv debug --headless --listen=:2345 --api-version=2 --log github.com/bananocoin/boompow/apps/client 3 | { 4 | "version": "0.2.0", 5 | "configurations": [ 6 | { 7 | "name": "Launch Debugger", 8 | "type": "go", 9 | "request": "attach", 10 | "mode": "remote", 11 | "remotePath": "/app", 12 | "port": 2345, 13 | "showLog": true, 14 | "trace": "verbose" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local dev data 2 | .data/*/** 3 | !.data/*/.gitkeep 4 | .env 5 | .DS_Store 6 | 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | __debug_bin 14 | *.zip 15 | 16 | # Test binary, built with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Dependency directories (remove the comment below to include it) 23 | # vendor/ 24 | 25 | apps/client/target -------------------------------------------------------------------------------- /apps/server/src/email/models.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | type ConfirmationEmailData struct { 4 | ConfirmationLink string 5 | ConfirmCodeExpirationDuration int 6 | IsProvider bool 7 | } 8 | 9 | type ResetPasswordEmailData struct { 10 | ResetPasswordLink string 11 | } 12 | 13 | type ConfirmServiceEmailData struct { 14 | EmailAddress string 15 | ServiceName string 16 | ServiceWebsite string 17 | ApproveServiceLink string 18 | } 19 | -------------------------------------------------------------------------------- /libs/utils/net/headers.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Get a clients real user IP Address 8 | func GetIPAddress(r *http.Request) string { 9 | IPAddress := r.Header.Get("CF-Connecting-IP") 10 | if IPAddress == "" { 11 | IPAddress = r.Header.Get("X-Real-Ip") 12 | } 13 | if IPAddress == "" { 14 | IPAddress = r.Header.Get("X-Forwarded-For") 15 | } 16 | if IPAddress == "" { 17 | IPAddress = r.RemoteAddr 18 | } 19 | return IPAddress 20 | } 21 | -------------------------------------------------------------------------------- /apps/server/graph/resolver.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/bananocoin/boompow/apps/server/src/repository" 7 | ) 8 | 9 | // This file will not be regenerated automatically. 10 | // 11 | // It serves as dependency injection for your app, add any dependencies you require here. 12 | 13 | type Resolver struct { 14 | UserRepo repository.UserRepo 15 | WorkRepo repository.WorkRepo 16 | PaymentRepo repository.PaymentRepo 17 | PrecacheMap *sync.Map 18 | } 19 | -------------------------------------------------------------------------------- /services/moneybags/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM golang:1.19-alpine AS build 2 | 3 | WORKDIR /src 4 | ARG TARGETOS TARGETARCH 5 | RUN --mount=target=. \ 6 | --mount=type=cache,target=/root/.cache/go-build \ 7 | --mount=type=cache,target=/go/pkg \ 8 | GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/moneybags /src/services/moneybags 9 | 10 | FROM alpine 11 | 12 | # Copy binary 13 | COPY --from=build /out/moneybags /bin 14 | 15 | EXPOSE 8080 16 | 17 | CMD ["moneybags", "--dry-run"] -------------------------------------------------------------------------------- /libs/utils/misc/slice_test.go: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import ( 4 | "testing" 5 | 6 | utils "github.com/bananocoin/boompow/libs/utils/testing" 7 | ) 8 | 9 | func TestContains(t *testing.T) { 10 | arrayString := []string{"a", "b", "c"} 11 | arrayInt := []int{0, 1, 2} 12 | 13 | utils.AssertEqual(t, true, Contains(arrayString, "a")) 14 | utils.AssertEqual(t, false, Contains(arrayString, "d")) 15 | utils.AssertEqual(t, true, Contains(arrayInt, 0)) 16 | utils.AssertEqual(t, false, Contains(arrayInt, 3)) 17 | } 18 | -------------------------------------------------------------------------------- /libs/utils/auth/crypt.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | //HashPassword hashes given password 6 | func HashPassword(password string) (string, error) { 7 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) 8 | return string(bytes), err 9 | } 10 | 11 | //CheckPassword hash compares raw password with it's hashed values 12 | func CheckPasswordHash(password, hash string) bool { 13 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 14 | return err == nil 15 | } 16 | -------------------------------------------------------------------------------- /apps/server/src/models/stats_singleton.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/bananocoin/boompow/apps/server/graph/model" 7 | ) 8 | 9 | var lock = &sync.Mutex{} 10 | 11 | type StatsSingleton struct { 12 | Stats *model.Stats 13 | } 14 | 15 | var statsInstance *StatsSingleton 16 | 17 | func GetStatsInstance() *StatsSingleton { 18 | if statsInstance == nil { 19 | lock.Lock() 20 | defer lock.Unlock() 21 | if statsInstance == nil { 22 | statsInstance = &StatsSingleton{} 23 | } 24 | } 25 | 26 | return statsInstance 27 | } 28 | -------------------------------------------------------------------------------- /libs/utils/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bananocoin/boompow/libs/utils 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/golang-jwt/jwt/v4 v4.4.2 7 | github.com/stretchr/testify v1.8.0 8 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa 9 | k8s.io/klog/v2 v2.80.1 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/go-logr/logr v1.2.3 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /apps/server/src/models/payment.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/bananocoin/boompow/libs/models" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | // Store the payment request to users in the database 9 | type Payment struct { 10 | Base 11 | BlockHash *string `json:"block_hash" gorm:"uniqueIndex"` 12 | SendId string `json:"send_id" gorm:"uniqueIndex;not null"` 13 | Amount uint `json:"amount"` 14 | SendJson models.SendRequest `json:"send_json" gorm:"type:jsonb;not null"` 15 | PaidTo uuid.UUID `json:"user_id" gorm:"not null"` 16 | } 17 | -------------------------------------------------------------------------------- /libs/models/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bananocoin/boompow/libs/utils v0.0.0-20220810021633-b4ba8d652a46 h1:NdQo+pwtopComoafV0vNqE3Gm2mQ5wFoFCaCvtMmgCo= 2 | github.com/bananocoin/boompow/libs/utils v0.0.0-20220810021633-b4ba8d652a46/go.mod h1:riRME+pAXYOOintUzsItUvySm+Ulo+Bc3IPBYryfGWU= 3 | github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= 4 | github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= 5 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 6 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | -------------------------------------------------------------------------------- /apps/server/src/models/types.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Types represent custom enums in postgres 4 | 5 | import ( 6 | "database/sql/driver" 7 | ) 8 | 9 | // The name of the type as it's stored in postgres 10 | const PG_USER_TYPE_NAME = "user_type" 11 | 12 | type UserType string 13 | 14 | const ( 15 | PROVIDER UserType = "PROVIDER" 16 | REQUESTER UserType = "REQUESTER" 17 | ) 18 | 19 | func (ct *UserType) Scan(value interface{}) error { 20 | *ct = UserType(value.(string)) 21 | return nil 22 | } 23 | 24 | func (ct UserType) Value() (driver.Value, error) { 25 | return string(ct), nil 26 | } 27 | -------------------------------------------------------------------------------- /apps/server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM golang:1.19-alpine AS build 2 | 3 | WORKDIR /src 4 | ARG TARGETOS TARGETARCH 5 | RUN --mount=target=. \ 6 | --mount=type=cache,target=/root/.cache/go-build \ 7 | --mount=type=cache,target=/go/pkg \ 8 | GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/boompow-server /src/apps/server 9 | 10 | FROM alpine 11 | 12 | # Copy email templates 13 | ADD ./apps/server/src/email/templates /src/apps/server/src/email/templates 14 | 15 | # Copy binary 16 | COPY --from=build /out/boompow-server /bin 17 | 18 | EXPOSE 8080 19 | 20 | CMD ["boompow-server", "server"] -------------------------------------------------------------------------------- /apps/server/src/models/work_result.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/google/uuid" 4 | 5 | type WorkResult struct { 6 | Base 7 | Hash string `json:"hash" gorm:"uniqueIndex;not null"` 8 | DifficultyMultiplier int `json:"difficulty_multiplier"` 9 | Result string `json:"result" gorm:"not null"` 10 | Awarded bool `json:"awarded" gorm:"default:false;not null"` // Whether or not this has been awarded 11 | ProvidedBy uuid.UUID `json:"providedBy" gorm:"not null"` 12 | RequestedBy uuid.UUID `json:"requestedBy" gorm:"not null"` 13 | Precache bool `json:"precache" gorm:"default:false;not null"` 14 | } 15 | -------------------------------------------------------------------------------- /libs/models/work_response_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | utils "github.com/bananocoin/boompow/libs/utils/testing" 8 | ) 9 | 10 | func TestSerializeDeserializeWorkResponse(t *testing.T) { 11 | workResponse := ClientWorkResponse{ 12 | RequestID: "123", 13 | Hash: "hash", 14 | Result: "3", 15 | } 16 | 17 | bytes, err := json.Marshal(workResponse) 18 | 19 | utils.AssertEqual(t, nil, err) 20 | 21 | var deserialized map[string]interface{} 22 | err = json.Unmarshal(bytes, &deserialized) 23 | 24 | utils.AssertEqual(t, "123", deserialized["request_id"]) 25 | utils.AssertEqual(t, "hash", deserialized["hash"]) 26 | utils.AssertEqual(t, "3", deserialized["result"]) 27 | } 28 | -------------------------------------------------------------------------------- /scripts/debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Simple helper to start debugging easier 4 | # Run this, then run the "Launch Debugger" task in vscode 5 | 6 | usage() { echo "Usage: $0 [-t ]" 1>&2; exit 1; } 7 | 8 | unset -v type 9 | 10 | while getopts t: opt; do 11 | case $opt in 12 | t) type=$OPTARG ;; 13 | *) usage ;; 14 | esac 15 | done 16 | 17 | shift "$(( OPTIND - 1 ))" 18 | 19 | if [ -z "$type" ]; then 20 | usage 21 | exit 1 22 | fi 23 | 24 | if [ "$type" != "server" ] && [ "$type" != "client" ]; then 25 | usage 26 | exit 1 27 | fi 28 | 29 | if [ "$type" == "server" ]; then 30 | dlv debug --headless --listen=:2345 --api-version=2 --log github.com/bananocoin/boompow/apps/server -- server 31 | elif [ "$type" == "client" ]; then 32 | dlv debug --headless --listen=:2345 --api-version=2 --log github.com/bananocoin/boompow/apps/client 33 | fi 34 | -------------------------------------------------------------------------------- /libs/utils/validation/password.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "unicode" 6 | ) 7 | 8 | // 8 characters long, at least one lowercase letter, one uppercase letter, one number, one special character 9 | func ValidatePassword(p string) error { 10 | if len(p) < 8 { 11 | return fmt.Errorf("password must be at least 8 characters long") 12 | } 13 | next: 14 | for name, classes := range map[string][]*unicode.RangeTable{ 15 | "upper case": {unicode.Upper, unicode.Title}, 16 | "lower case": {unicode.Lower}, 17 | "numeric": {unicode.Number, unicode.Digit}, 18 | "special": {unicode.Space, unicode.Symbol, unicode.Punct, unicode.Mark}, 19 | } { 20 | for _, r := range p { 21 | if unicode.IsOneOf(classes, r) { 22 | continue next 23 | } 24 | } 25 | return fmt.Errorf("password must have at least one %s character", name) 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /apps/server/src/models/base.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // Base contains common columns for all tables 11 | type Base struct { 12 | ID uuid.UUID `json:"_id" gorm:"primaryKey;autoIncrement:false"` 13 | CreatedAt time.Time `json:"created_at"` 14 | UpdatedAt time.Time `json:"updated_at"` 15 | } 16 | 17 | // BeforeCreate will set Base struct before every insert 18 | func (base *Base) BeforeCreate(tx *gorm.DB) error { 19 | // uuid.New() creates a new random UUID or panics. 20 | base.ID = uuid.New() 21 | 22 | // generate timestamps 23 | now := time.Now().UTC() 24 | base.CreatedAt, base.UpdatedAt = now, now 25 | 26 | return nil 27 | } 28 | 29 | // AfterUpdate will update the Base struct after every update 30 | func (base *Base) AfterUpdate(tx *gorm.DB) error { 31 | // update timestamps 32 | base.UpdatedAt = time.Now().UTC() 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /apps/client/windows/run-with-options.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | :: Don't change me 4 | set true=1==1 5 | set false=1==0 6 | 7 | :: Username (email) If you don't want to be prompted for email on startup 8 | set email="" 9 | 10 | :: Password (password) - If you don't want to be prompted for password on startup 11 | set password="" 12 | 13 | :: GPU Only - If true, will only compute PoW on GPU, otherwise will use both CPU and GPU 14 | set gpuonly=%false% 15 | 16 | :: Max work difficulty 17 | set max_difficulty_multiplier=128 18 | 19 | :: Min work difficulty 20 | set min_difficulty_multiplier=1 21 | 22 | echo Starting BoomPow Client... 23 | 24 | if %gpuonly% ( 25 | boompow-client.exe -email %email% -password %password% -max-difficulty %max_difficulty_multiplier% -min-difficulty %min_difficulty_multiplier% -gpu-only 26 | ) else ( 27 | boompow-client.exe -email %email% -password %password% -max-difficulty %max_difficulty_multiplier% -min-difficulty %min_difficulty_multiplier% 28 | ) 29 | 30 | pause -------------------------------------------------------------------------------- /libs/models/client_message.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type MessageType string 4 | 5 | const ( 6 | WorkGenerate MessageType = "work_generate" 7 | WorkCancel MessageType = "work_cancel" 8 | BlockAwarded MessageType = "block_awarded" 9 | ) 10 | 11 | // Message sent from server -> client 12 | type ClientMessage struct { 13 | // Exclude this field from serialization (don't expose requester email to client) 14 | RequesterEmail string `json:"-"` 15 | BlockAward bool `json:"-"` 16 | MessageType MessageType `json:"request_type"` 17 | // We attach a unique request ID to each request, this links it to user requesting work 18 | RequestID string `json:"request_id"` 19 | Hash string `json:"hash"` 20 | DifficultyMultiplier int `json:"difficulty_multiplier"` 21 | // Awarded info 22 | ProviderEmail string `json:"-"` 23 | PercentOfPool float64 `json:"percent_of_pool"` 24 | EstimatedAward float64 `json:"estimated_award"` 25 | Precache bool `json:"precache"` 26 | } 27 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM golang:1.19-alpine 2 | 3 | ARG ZSH_IN_DOCKER_VERSION=1.1.2 4 | 5 | 6 | # Install useful tools 7 | RUN apk --no-cache upgrade && \ 8 | apk --no-cache add ca-certificates curl gnupg jq bash tar openssl util-linux nano zsh-syntax-highlighting less procps lsof postgresql-client alpine-sdk 9 | 10 | # Install zsh-in-docker 11 | RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \ 12 | -t clean \ 13 | -p git \ 14 | -p node \ 15 | -p yarn \ 16 | -p history \ 17 | -p https://github.com/zsh-users/zsh-autosuggestions \ 18 | -p https://github.com/zsh-users/zsh-completions 19 | RUN echo "source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" >> /root/.zshrc 20 | 21 | # Working directory inside container 22 | WORKDIR /app 23 | 24 | EXPOSE 3000 25 | 26 | RUN go install github.com/go-delve/delve/cmd/dlv@latest && go install github.com/99designs/gqlgen@latest && go install github.com/Khan/genqlient@latest 27 | 28 | CMD [ "/bin/zsh", "-c" ] 29 | -------------------------------------------------------------------------------- /libs/models/nano.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // RPC requests 11 | type BaseRequest struct { 12 | Action string `json:"action"` 13 | } 14 | 15 | // send 16 | var SendAction BaseRequest = BaseRequest{Action: "send"} 17 | 18 | type SendRequest struct { 19 | BaseRequest 20 | Wallet string `json:"wallet"` 21 | Source string `json:"source"` 22 | Destination string `json:"destination"` 23 | AmountRaw string `json:"amount"` 24 | ID string `json:"id"` 25 | PaidTo uuid.UUID `json:"-"` 26 | } 27 | 28 | type SendResponse struct { 29 | Block string `json:"block"` 30 | } 31 | 32 | // Type of nano payment object as JSONB 33 | func (j SendRequest) Value() (driver.Value, error) { 34 | valueString, err := json.Marshal(j) 35 | return string(valueString), err 36 | } 37 | 38 | func (j *SendRequest) Scan(value interface{}) error { 39 | if err := json.Unmarshal(value.([]byte), &j); err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Appditto LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/utils/auth/jwt_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/hex" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | utils "github.com/bananocoin/boompow/libs/utils/testing" 10 | ) 11 | 12 | var now = func() time.Time { 13 | return time.Unix(0, 0) 14 | } 15 | 16 | func TestGenerateToken(t *testing.T) { 17 | os.Setenv("PRIV_KEY", "value") 18 | defer os.Unsetenv("PRIV_KEY") 19 | token, _ := GenerateToken("joe@gmail.com", now) 20 | utils.AssertEqual(t, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvZUBnbWFpbC5jb20iLCJleHAiOjg2NDAwfQ.4EWNyTndi4_6yT8JlA9RWjVIC6p2BiKAJx3BHGA4qYM", token) 21 | } 22 | 23 | func TestParseToken(t *testing.T) { 24 | os.Setenv("PRIV_KEY", "value") 25 | defer os.Unsetenv("PRIV_KEY") 26 | token, _ := GenerateToken("joe@gmail.com", time.Now) 27 | parsed, _ := ParseToken(token) 28 | utils.AssertEqual(t, "joe@gmail.com", parsed) 29 | } 30 | 31 | func TestGenerateRandHexString(t *testing.T) { 32 | gen, _ := GenerateRandHexString() 33 | parsed, err := hex.DecodeString(gen) 34 | 35 | utils.AssertEqual(t, nil, err) 36 | utils.AssertEqual(t, 64, len(gen)) 37 | utils.AssertEqual(t, 32, len(parsed)) 38 | } 39 | -------------------------------------------------------------------------------- /apps/client/work/benchmark.go: -------------------------------------------------------------------------------- 1 | package work 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/Inkeliz/go-opencl/opencl" 10 | "github.com/bananocoin/boompow/libs/models" 11 | ) 12 | 13 | func RunBenchmark(nHashes int, difficultyMultiplier int, gpuOnly bool, devices []opencl.Device) { 14 | workPool := NewWorkPool(gpuOnly, devices) 15 | if difficultyMultiplier < 1 { 16 | difficultyMultiplier = 1 17 | } 18 | totalDelta := 0.0 19 | for i := 0; i < nHashes; i++ { 20 | bytes := make([]byte, 32) 21 | if _, err := rand.Read(bytes); err != nil { 22 | panic("Failed to generate hash") 23 | } 24 | 25 | fmt.Printf("\nRun %d", i+1) 26 | startT := time.Now() 27 | 28 | _, err := workPool.WorkGenerate(&models.ClientMessage{ 29 | Hash: hex.EncodeToString(bytes), 30 | DifficultyMultiplier: difficultyMultiplier, 31 | }) 32 | if err != nil { 33 | panic("Failed to generate work") 34 | } 35 | endT := time.Now() 36 | delta := endT.Sub(startT).Seconds() 37 | totalDelta += delta 38 | fmt.Printf("\nTook: %fs", delta) 39 | } 40 | fmt.Printf("\n\nAverage: %fs", totalDelta/float64(nHashes)) 41 | } 42 | -------------------------------------------------------------------------------- /libs/utils/validation/work_test.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "testing" 5 | 6 | utils "github.com/bananocoin/boompow/libs/utils/testing" 7 | ) 8 | 9 | func TestWorkValidation(t *testing.T) { 10 | // Valid result 11 | workResult := "205452237a9b01f4" 12 | hash := "3F93C5CD2E314FA16702189041E68E68C07B27961BF37F0B7705145BEFBA3AA3" 13 | 14 | // Check that we can access these items 15 | utils.AssertEqual(t, true, IsWorkValid(hash, 1, workResult)) 16 | 17 | // Invalid result 18 | workResult = "205452237a9b01f4" 19 | hash = "3F93C5CD2E314FA16702189041E68E68C07B27961BF37F0B7705145BEFBA3AA3" 20 | 21 | // Check that we can access these items 22 | utils.AssertEqual(t, false, IsWorkValid(hash, 800, workResult)) 23 | 24 | // Invalid result 25 | workResult = "205452237a9b01f4" 26 | hash = "F1C59E6C738BB82221E082910740BADC58301F8F32291E07CCC4CDBEEAD44348" 27 | 28 | // Check that we can access these items 29 | utils.AssertEqual(t, false, IsWorkValid(hash, 1, workResult)) 30 | 31 | hash = "03DDDFF29D3FF3DC41B5374A10A70B49F7AA41E42461511D6A64F346F9C8421E" 32 | workResult = "00000000002d7708" 33 | utils.AssertEqual(t, false, IsWorkValid(hash, 1, workResult)) 34 | } 35 | -------------------------------------------------------------------------------- /apps/server/src/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type User struct { 6 | Base 7 | Type UserType `gorm:"type:user_type;not null"` 8 | Email string `json:"email" gorm:"uniqueIndex;not null"` 9 | Password string `json:"password" gorm:"not null"` 10 | EmailVerified bool `json:"emailVerfied" gorm:"default:false;not null"` 11 | ServiceName *string `json:"serviceName"` 12 | ServiceWebsite *string `json:"serviceWebsite"` 13 | CanRequestWork bool `json:"canRequestWork" gorm:"default:false;not null"` 14 | InvalidResultCount int `json:"invalidResultCount" gorm:"default:0;not null"` 15 | // For reward payments 16 | BanAddress *string `json:"banAddress"` 17 | // The work this user provider 18 | WorkResults []WorkResult `gorm:"foreignKey:ProvidedBy"` 19 | LastProvidedWorkAt *time.Time `json:"lastProvidedWorkAt"` 20 | // The work this user has requested 21 | WorkRequests []WorkResult `gorm:"foreignKey:RequestedBy"` 22 | LastRequestedWorkAt *time.Time `json:"lastRequestedWorkAt"` 23 | // Payments sent to this user 24 | Payments []Payment `gorm:"foreignKey:PaidTo"` 25 | // Banned 26 | Banned bool `json:"banned" gorm:"default:false;not null"` 27 | } 28 | -------------------------------------------------------------------------------- /apps/client/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bananocoin/boompow/apps/client 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/Inkeliz/go-opencl v0.0.0-20200806180703-5f0707fba006 7 | github.com/Khan/genqlient v0.5.0 8 | github.com/bananocoin/boompow/libs/models v0.0.0-20220813160408-80dcb738fae5 9 | github.com/bananocoin/boompow/libs/utils v0.0.0-20220813160408-80dcb738fae5 10 | github.com/bbedward/nanopow v0.0.0-20220813154520-94e2401a7737 11 | github.com/go-co-op/gocron v1.16.2 12 | github.com/gorilla/websocket v1.5.0 13 | github.com/jpillora/backoff v1.0.0 14 | github.com/mbndr/figlet4go v0.0.0-20190224160619-d6cef5b186ea 15 | golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 16 | k8s.io/klog/v2 v2.70.1 17 | ) 18 | 19 | require ( 20 | github.com/go-logr/logr v1.2.0 // indirect 21 | github.com/golang/glog v1.0.0 // indirect 22 | github.com/google/uuid v1.3.0 // indirect 23 | github.com/robfig/cron/v3 v3.0.1 // indirect 24 | github.com/vektah/gqlparser/v2 v2.4.7 // indirect 25 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 26 | golang.org/x/exp/errors v0.0.0-20220722155223-a9213eeb770e // indirect 27 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect 28 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /libs/models/client_message_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "testing" 7 | 8 | utils "github.com/bananocoin/boompow/libs/utils/testing" 9 | ) 10 | 11 | func TestSerializeDeserializeClientRequest(t *testing.T) { 12 | workRequest := ClientMessage{ 13 | RequesterEmail: "notserialized@gmail.com", 14 | MessageType: WorkGenerate, 15 | RequestID: "123", 16 | Hash: "hash", 17 | DifficultyMultiplier: 3, 18 | Precache: true, 19 | } 20 | 21 | // Just want to ensure we don't leak requester emails to the client 22 | bytes, err := json.Marshal(workRequest) 23 | str := string(bytes) 24 | utils.AssertEqual(t, false, strings.Contains(str, "notserialized@gmail.com")) 25 | 26 | utils.AssertEqual(t, nil, err) 27 | 28 | var deserialized map[string]interface{} 29 | err = json.Unmarshal(bytes, &deserialized) 30 | 31 | utils.AssertEqual(t, nil, deserialized["requester_email"]) 32 | utils.AssertEqual(t, "work_generate", deserialized["request_type"]) 33 | utils.AssertEqual(t, "123", deserialized["request_id"]) 34 | utils.AssertEqual(t, "hash", deserialized["hash"]) 35 | utils.AssertEqual(t, float64(3), deserialized["difficulty_multiplier"]) 36 | utils.AssertEqual(t, true, deserialized["precache"]) 37 | } 38 | -------------------------------------------------------------------------------- /apps/client/models/random_access_queue_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | serializableModels "github.com/bananocoin/boompow/libs/models" 8 | utils "github.com/bananocoin/boompow/libs/utils/testing" 9 | ) 10 | 11 | // Test random access map 12 | func TestRandomAccessMap(t *testing.T) { 13 | // Seed random for consistency 14 | rand.Seed(1) 15 | queue := NewRandomAccessQueue() 16 | 17 | // Add a few items 18 | queue.Put(serializableModels.ClientMessage{ 19 | RequestID: "1", 20 | Hash: "1", 21 | DifficultyMultiplier: 1, 22 | }) 23 | queue.Put(serializableModels.ClientMessage{ 24 | RequestID: "2", 25 | Hash: "2", 26 | DifficultyMultiplier: 2, 27 | }) 28 | queue.Put(serializableModels.ClientMessage{ 29 | RequestID: "3", 30 | Hash: "3", 31 | DifficultyMultiplier: 3, 32 | }) 33 | 34 | // Check that we can access these items 35 | utils.AssertEqual(t, "1", queue.Get("1").Hash) 36 | 37 | // Check that we can pop a random item 38 | utils.AssertEqual(t, "3", queue.PopRandom().Hash) 39 | 40 | // Check that popped item is removed 41 | utils.AssertEqual(t, (*serializableModels.ClientMessage)(nil), queue.Get("3")) 42 | 43 | // Check length 44 | utils.AssertEqual(t, 2, queue.Len()) 45 | } 46 | -------------------------------------------------------------------------------- /libs/utils/net/headers_test.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "testing" 7 | 8 | utils "github.com/bananocoin/boompow/libs/utils/testing" 9 | ) 10 | 11 | func TestGetIPAddressFromHeader(t *testing.T) { 12 | ip := "123.45.67.89" 13 | 14 | // 4 methods of getting IP Address, CF-Connecting-IP preferred, X-Real-Ip, then X-Forwarded-For, then RemoteAddr 15 | 16 | request, _ := http.NewRequest(http.MethodPost, "appditto.com", bytes.NewReader([]byte(""))) 17 | request.Header.Set("CF-Connecting-IP", ip) 18 | request.Header.Set("X-Real-Ip", "not-the-ip") 19 | request.Header.Set("X-Forwarded-For", "not-the-ip") 20 | utils.AssertEqual(t, ip, GetIPAddress(request)) 21 | 22 | request, _ = http.NewRequest(http.MethodPost, "appditto.com", bytes.NewReader([]byte(""))) 23 | request.Header.Set("X-Real-Ip", ip) 24 | request.Header.Set("X-Forwarded-For", "not-the-ip") 25 | 26 | utils.AssertEqual(t, ip, GetIPAddress(request)) 27 | 28 | request, _ = http.NewRequest(http.MethodPost, "appditto.com", bytes.NewReader([]byte(""))) 29 | request.Header.Set("X-Forwarded-For", ip) 30 | utils.AssertEqual(t, ip, GetIPAddress(request)) 31 | 32 | request, _ = http.NewRequest(http.MethodPost, "appditto.com", bytes.NewReader([]byte(""))) 33 | request.RemoteAddr = ip 34 | utils.AssertEqual(t, ip, GetIPAddress(request)) 35 | } 36 | -------------------------------------------------------------------------------- /apps/server/src/models/sync_array_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | utils "github.com/bananocoin/boompow/libs/utils/testing" 7 | ) 8 | 9 | // Test sync array 10 | func TestSyncArray(t *testing.T) { 11 | array := NewSyncArray() 12 | 13 | // Add a few items 14 | array.Put(&ActiveChannelObject{ 15 | RequesterEmail: "1", 16 | RequestID: "1", 17 | Hash: "1", 18 | DifficultyMultiplier: 1, 19 | Chan: make(chan []byte), 20 | }) 21 | array.Put(&ActiveChannelObject{ 22 | RequesterEmail: "2", 23 | RequestID: "2", 24 | Hash: "2", 25 | DifficultyMultiplier: 2, 26 | Chan: make(chan []byte), 27 | }) 28 | array.Put(&ActiveChannelObject{ 29 | RequesterEmail: "3", 30 | RequestID: "3", 31 | Hash: "3", 32 | DifficultyMultiplier: 3, 33 | Chan: make(chan []byte), 34 | }) 35 | 36 | utils.AssertEqual(t, 3, array.Len()) 37 | utils.AssertEqual(t, true, array.Exists("1")) 38 | utils.AssertEqual(t, "1", array.Get("1").Hash) 39 | utils.AssertEqual(t, true, array.HashExists("2")) 40 | array.Delete("1") 41 | utils.AssertEqual(t, (*ActiveChannelObject)(nil), array.Get("1")) 42 | utils.AssertEqual(t, 2, array.Len()) 43 | utils.AssertEqual(t, 0, array.IndexOf("3")) 44 | } 45 | -------------------------------------------------------------------------------- /libs/utils/validation/banano_test.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | utils "github.com/bananocoin/boompow/libs/utils/testing" 8 | ) 9 | 10 | func TestReverseOrder(t *testing.T) { 11 | str := []byte{1, 2, 3} 12 | 13 | utils.AssertEqual(t, []byte{3, 2, 1}, Reversed(str)) 14 | } 15 | 16 | func TestReverseUnordered(t *testing.T) { 17 | str := []byte{1, 2, 1, 3, 1} 18 | 19 | utils.AssertEqual(t, []byte{1, 3, 1, 2, 1}, Reversed(str)) 20 | } 21 | 22 | func TestAddressToPub(t *testing.T) { 23 | pub, _ := AddressToPub("ban_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3") 24 | 25 | utils.AssertEqual(t, "e89208dd038fbb269987689621d52292ae9c35941a7484756ecced92a65093ba", hex.EncodeToString(pub)) 26 | } 27 | 28 | func TestValidateAddress(t *testing.T) { 29 | // Valid 30 | valid := "ban_1zyb1s96twbtycqwgh1o6wsnpsksgdoohokikgjqjaz63pxnju457pz8tm3r" 31 | utils.AssertEqual(t, true, ValidateAddress(valid)) 32 | // Invalid 33 | invalid := "ban_1zyb1s96twbtycqwgh1o6wsnpsksgdoohokikgjqjaz63pxnju457pz8tm3ra" 34 | utils.AssertEqual(t, false, ValidateAddress(invalid)) 35 | invalid = "ban_1zyb1s96twbtycqwgh1o6wsnpsksgdoohokikgjqjaz63pxnju457pz8tm3rb" 36 | utils.AssertEqual(t, false, ValidateAddress(invalid)) 37 | invalid = "nano_1zyb1s96twbtycqwgh1o6wsnpsksgdoohokikgjqjaz63pxnju457pz8tm3r" 38 | utils.AssertEqual(t, false, ValidateAddress(invalid)) 39 | } 40 | -------------------------------------------------------------------------------- /libs/utils/number/banano.go: -------------------------------------------------------------------------------- 1 | package number 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "math/big" 8 | ) 9 | 10 | const rawPerBananoStr = "100000000000000000000000000000" 11 | 12 | var rawPerBanano, _ = new(big.Float).SetString(rawPerBananoStr) 13 | 14 | const bananoPrecision = 100 // 0.01 BANANO precision 15 | 16 | // Raw to Big - converts raw amount to a big.Int 17 | func RawToBigInt(raw string) (*big.Int, error) { 18 | rawBig, ok := new(big.Int).SetString(raw, 10) 19 | if !ok { 20 | return nil, errors.New(fmt.Sprintf("Unable to convert %s to big int", raw)) 21 | } 22 | return rawBig, nil 23 | } 24 | 25 | // RawToBanano - Converts Raw amount to usable Banano amount 26 | func RawToBanano(raw string, truncate bool) (float64, error) { 27 | rawBig, ok := new(big.Float).SetString(raw) 28 | if !ok { 29 | err := errors.New(fmt.Sprintf("Unable to convert %s to int", raw)) 30 | return -1, err 31 | } 32 | asBanano := rawBig.Quo(rawBig, rawPerBanano) 33 | f, _ := asBanano.Float64() 34 | if !truncate { 35 | return f, nil 36 | } 37 | 38 | return math.Trunc(f/0.01) * 0.01, nil 39 | } 40 | 41 | // BananoToRaw - Converts Banano amount to Raw amount 42 | func BananoToRaw(banano float64) string { 43 | bananoInt := int(banano * 100) 44 | bananoRaw, _ := new(big.Int).SetString("1000000000000000000000000000", 10) 45 | 46 | res := bananoRaw.Mul(bananoRaw, big.NewInt(int64(bananoInt))) 47 | 48 | return fmt.Sprintf("%d", res) 49 | } 50 | -------------------------------------------------------------------------------- /libs/utils/validation/work.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | 7 | "golang.org/x/crypto/blake2b" 8 | ) 9 | 10 | const ( 11 | baseMaxUint64 = uint64(1<<64 - 1) 12 | baseDifficulty = baseMaxUint64 - uint64(0xfffffe0000000000) 13 | ) 14 | 15 | func CalculateDifficulty(multiplier int64) uint64 { 16 | if multiplier < 0 { 17 | return baseMaxUint64 - (baseDifficulty * ((baseMaxUint64 - uint64(multiplier)) + 1)) 18 | } 19 | 20 | if multiplier == 0 { 21 | multiplier = 1 22 | } 23 | 24 | return baseMaxUint64 - (baseDifficulty / uint64(multiplier)) 25 | } 26 | 27 | func IsWorkValid(previous string, difficultyMultiplier int, w string) bool { 28 | difficult := CalculateDifficulty(int64(difficultyMultiplier)) 29 | previousEnc, err := hex.DecodeString(previous) 30 | if err != nil { 31 | return false 32 | } 33 | wEnc, err := hex.DecodeString(w) 34 | if err != nil { 35 | return false 36 | } 37 | 38 | hash, err := blake2b.New(8, nil) 39 | if err != nil { 40 | return false 41 | } 42 | 43 | n := make([]byte, 8) 44 | copy(n, wEnc[:]) 45 | 46 | reverse(n) 47 | hash.Write(n) 48 | hash.Write(previousEnc[:]) 49 | 50 | return binary.LittleEndian.Uint64(hash.Sum(nil)) >= difficult 51 | } 52 | 53 | func reverse(v []byte) { 54 | // binary.LittleEndian.PutUint64(v, binary.BigEndian.Uint64(v)) 55 | v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7] = v[7], v[6], v[5], v[4], v[3], v[2], v[1], v[0] // It's works. LOL 56 | } 57 | -------------------------------------------------------------------------------- /libs/utils/auth/jwt.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "log" 7 | "time" 8 | 9 | "github.com/bananocoin/boompow/libs/utils" 10 | "github.com/golang-jwt/jwt/v4" 11 | ) 12 | 13 | // secret key being used to sign tokens 14 | var ( 15 | SecretKey = utils.GetJwtKey() 16 | ) 17 | 18 | // GenerateToken generates a jwt token and assign a email to it's claims and return it 19 | func GenerateToken(email string, nowFunc func() time.Time) (string, error) { 20 | token := jwt.New(jwt.SigningMethodHS256) 21 | /* Create a map to store our claims */ 22 | claims := token.Claims.(jwt.MapClaims) 23 | /* Set token claims */ 24 | claims["email"] = email 25 | claims["exp"] = nowFunc().Add(time.Hour * 24).Unix() 26 | tokenString, err := token.SignedString(SecretKey) 27 | if err != nil { 28 | log.Fatal("Error in Generating key") 29 | return "", err 30 | } 31 | return tokenString, nil 32 | } 33 | 34 | // ParseToken parses a jwt token and returns the email in it's claims 35 | func ParseToken(tokenStr string) (string, error) { 36 | token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) { 37 | return SecretKey, nil 38 | }) 39 | if err != nil { 40 | return "", err 41 | } 42 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 43 | email := claims["email"].(string) 44 | return email, nil 45 | } else { 46 | return "", err 47 | } 48 | } 49 | 50 | // Generate random 32-byte hex string 51 | func GenerateRandHexString() (string, error) { 52 | bytes := make([]byte, 32) 53 | if _, err := rand.Read(bytes); err != nil { 54 | return "", err 55 | } 56 | return hex.EncodeToString(bytes), nil 57 | } 58 | -------------------------------------------------------------------------------- /libs/utils/validation/password_test.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | utils "github.com/bananocoin/boompow/libs/utils/testing" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestPasswordValidation(t *testing.T) { 12 | goodPass := "Password123!" 13 | 14 | utils.AssertEqual(t, nil, ValidatePassword(goodPass)) 15 | // All lower/upper are invalid 16 | assert.EqualErrorf(t, ValidatePassword(strings.ToLower(goodPass)), "password must have at least one upper case character", "Error should be: %v, got: %v", "password must have at least one upper case character", ValidatePassword(strings.ToLower(goodPass))) 17 | assert.EqualErrorf(t, ValidatePassword(strings.ToUpper(goodPass)), "password must have at least one lower case character", "Error should be: %v, got: %v", "password must have at least one lower case character", ValidatePassword(strings.ToUpper(goodPass))) 18 | // No number is invalid 19 | assert.EqualErrorf(t, ValidatePassword("Password!"), "password must have at least one numeric character", "Error should be: %v, got: %v", "password must have at least one numeric character", ValidatePassword("Password!")) 20 | // No special character is invalid 21 | assert.EqualErrorf(t, ValidatePassword("Password123"), "password must have at least one special character", "Error should be: %v, got: %v", "password must have at least one special character", ValidatePassword("Password123")) 22 | // 8 in length 23 | assert.EqualErrorf(t, ValidatePassword("Passwor"), "password must be at least 8 characters long", "Error should be: %v, got: %v", "password must be at least 8 characters long", ValidatePassword("Password")) 24 | } 25 | -------------------------------------------------------------------------------- /libs/utils/env_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | utils "github.com/bananocoin/boompow/libs/utils/testing" 8 | ) 9 | 10 | func TestGetEnv(t *testing.T) { 11 | os.Setenv("MY_ENV", "value") 12 | defer os.Unsetenv("MY_ENV") 13 | 14 | utils.AssertEqual(t, "value", GetEnv("MY_ENV", "default")) 15 | utils.AssertEqual(t, "default", GetEnv("MY_ENV_UNKNOWN", "default")) 16 | } 17 | 18 | func TestGetBannedRewards(t *testing.T) { 19 | os.Setenv("BPOW_BANNED_REWARDS", "a,b") 20 | defer os.Unsetenv("BPOW_BANNED_REWARDS") 21 | 22 | utils.AssertEqual(t, []string{"a", "b"}, GetBannedRewards()) 23 | } 24 | 25 | func TestGetJwtKey(t *testing.T) { 26 | os.Unsetenv("PRIV_KEY") 27 | utils.AssertEqual(t, []byte("badKey"), GetJwtKey()) 28 | 29 | os.Setenv("PRIV_KEY", "X") 30 | defer os.Unsetenv("PRIV_KEY") 31 | utils.AssertEqual(t, []byte("X"), GetJwtKey()) 32 | } 33 | 34 | func TestGetSmtpConnInformation(t *testing.T) { 35 | os.Setenv("SMTP_SERVER", "") 36 | os.Setenv("SMTP_PORT", "-1") 37 | os.Setenv("SMTP_USERNAME", "") 38 | os.Setenv("SMTP_PASSWORD", "") 39 | defer os.Unsetenv("SMTP_SERVER") 40 | defer os.Unsetenv("SMTP_PORT") 41 | defer os.Unsetenv("SMTP_USERNAME") 42 | defer os.Unsetenv("SMTP_PASSWORD") 43 | 44 | utils.AssertEqual(t, (*SmtpConnInformation)(nil), GetSmtpConnInformation()) 45 | 46 | os.Setenv("SMTP_SERVER", "abc.com") 47 | os.Setenv("SMTP_PORT", "1234") 48 | os.Setenv("SMTP_USERNAME", "joe") 49 | os.Setenv("SMTP_PASSWORD", "jeff") 50 | 51 | connInfo := GetSmtpConnInformation() 52 | utils.AssertEqual(t, "abc.com", connInfo.Server) 53 | utils.AssertEqual(t, 1234, connInfo.Port) 54 | utils.AssertEqual(t, "joe", connInfo.Username) 55 | utils.AssertEqual(t, "jeff", connInfo.Password) 56 | } 57 | -------------------------------------------------------------------------------- /apps/client/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Client 6 | 7 | This is the BoomPoW client, it receives and calculates work requests from the BoomPoW server 8 | 9 | ## Usage 10 | 11 | The BoomPoW binaries generate work using the GPU and CPU by default, if the GPU is not available, then it will use CPU only. 12 | 13 | You can build a version with CPU only by following the compilation instructions below. 14 | 15 | For AMD GPUs on linux, you will need to either use the `amdgpu-pro` driver or run the `amdgpu-installer` with the following: 16 | 17 | ``` 18 | amdgpu-install --usecase=opencl --no-dkms 19 | ``` 20 | 21 | ## Compiling 22 | 23 | ### Windows 24 | 25 | For windows, requirements are: 26 | 27 | - [GOLang](https://go.dev/doc/install) 28 | - [TDM-GCC](http://tdm-gcc.tdragon.net/download) 29 | - [OCL_SDK_Light](https://github.com/GPUOpen-LibrariesAndSDKs/OCL-SDK/releases) (For OpenCL) 30 | 31 | You can copy the `opencl.lib` to the `lib\x86_64` directory of TDM-GCC to compile with OpenCL support 32 | 33 | To build: 34 | 35 | ``` 36 | go build -o boompow-client.exe -tags cl -ldflags "-X main.WSUrl=wss://boompow.banano.cc/ws/worker -X main.GraphQLURL=https://boompow.banano.cc/graphql" . 37 | ``` 38 | 39 | To build for CPU only remove `-tags cl` 40 | 41 | ### Linux 42 | 43 | Each distribution will vary on requirements, but on a ubuntu/debian based build something like 44 | 45 | ``` 46 | sudo apt install golang-go build-essential ocl-icd-opencl-dev 47 | ``` 48 | 49 | should get you what you need. 50 | 51 | Then you can run `./build.sh` to build the binary 52 | 53 | ### MacOS 54 | 55 | MacOS is the same process as Linux, except you do not need to install OpenCL headers. 56 | -------------------------------------------------------------------------------- /kubernetes/moneybags/cron.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: CronJob 3 | metadata: 4 | name: moneybags 5 | namespace: boompow-next 6 | spec: 7 | schedule: "0 8 * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: boompow-payments 14 | image: replaceme 15 | command: ["/bin/sh", "-c"] 16 | args: ["moneybags"] 17 | env: 18 | - name: DB_HOST 19 | value: pg-boompow.boompow-next 20 | - name: DB_PORT 21 | value: "5432" 22 | - name: DB_SSLMODE 23 | value: disable 24 | - name: DB_NAME 25 | value: postgres 26 | - name: DB_USER 27 | value: postgres 28 | - name: DB_PASS 29 | valueFrom: 30 | secretKeyRef: 31 | name: boompow 32 | key: db_password 33 | - name: REDIS_HOST 34 | value: redis.redis 35 | - name: REDIS_DB 36 | value: "18" 37 | - name: BPOW_WALLET_ID 38 | valueFrom: 39 | secretKeyRef: 40 | name: boompow 41 | key: wallet_id 42 | - name: BPOW_PRIZE_POOL 43 | valueFrom: 44 | secretKeyRef: 45 | name: boompow 46 | key: prize_pool 47 | - name: BPOW_WALLET_ADDRESS 48 | value: ban_1boompow14irck1yauquqypt7afqrh8b6bbu5r93pc6hgbqs7z6o99frcuym 49 | - name: ENVIRONMENT 50 | value: production 51 | restartPolicy: OnFailure -------------------------------------------------------------------------------- /apps/server/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Server 6 | 7 | This is the BoomPoW server, it coordinates work requests from users/services to "providers" that use the client. 8 | 9 | ## About 10 | 11 | The server is written in [GOLang](https://go.dev) and requires Postgres and Redis, you can reference the [docker-compose.yaml](https://github.com/bananocoin/boompow/blob/master/docker-compose.yaml) in the workspace root for details on how to run the server. 12 | 13 | It provides a GraphQL API at `/graphql` and the schema can be seen [here](https://github.com/bananocoin/boompow/blob/master/apps/server/graph/schema.graphqls) 14 | 15 | A secure websocket endpoint is also available at `/ws/worker` this is the channel that the providers and server use to communicate work requests and responses. 16 | 17 | Users are broken up into 2 categories: 18 | 19 | 1. PROVIDER 20 | 21 | Providers are the users who are providing work to BoomPoW in exchange for rewards. 22 | 23 | 2. REQUESTER 24 | 25 | The requesters are work consumers, services that have access to request work from BoomPoW and by proxy the providers. 26 | 27 | Work is requested using the `workGenerate` mutation and requires authentication using a service token (not the JWT token returned from the `login` mutation). These tokens can be obtained using the `generateServiceToken` mutation. 28 | 29 | There are some layers on protection to prevent users from requesting work. 30 | 31 | 1. Email must be verified 32 | 2. `can_request_work` must be set to true in the database 33 | 34 | The second part is intended to happen manually, after a new service requests a key they will be manually approved, after which they can invoke the `generateServiceToken` mutation. 35 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | redis: 4 | container_name: bpow_redis 5 | image: redis:7-alpine 6 | restart: unless-stopped 7 | ports: 8 | - '127.0.0.1:6379:6379' 9 | networks: ['app-network'] 10 | 11 | db: 12 | container_name: bpow_postgres 13 | image: postgres:14 14 | ports: 15 | - '127.0.0.1:5433:5432' 16 | restart: unless-stopped 17 | environment: 18 | - POSTGRES_DB=boompow 19 | - POSTGRES_USER=postgres 20 | - POSTGRES_PASSWORD=postgres 21 | - PGDATA=/var/lib/postgresql/data/dev 22 | volumes: 23 | - .data/postgres:/var/lib/postgresql/data:delegated # Delegated indicates the containers view of the volume takes priority 24 | - ./scripts/setup_test_db.sh:/docker-entrypoint-initdb.d/setup_test_db.sh 25 | networks: ['app-network'] 26 | 27 | app: 28 | container_name: boompow_dev 29 | security_opt: 30 | - 'seccomp:unconfined' 31 | environment: 32 | - DB_HOST=db 33 | - DB_PORT=5432 34 | - DB_USER=postgres 35 | - DB_PASS=postgres 36 | - DB_NAME=boompow 37 | - DB_MOCK_HOST=db 38 | - DB_MOCK_PORT=5432 39 | - DB_MOCK_USER=postgres 40 | - DB_MOCK_PASS=postgres 41 | - DB_SSLMODE=disable 42 | - DATABASE_URL=postgres://postgres:postgres@db:5432/boompow 43 | - REDIS_HOST=redis 44 | - GOPRIVATE=github.com/bananocoin 45 | ports: 46 | - '127.0.0.1:8081:8080' 47 | - '127.0.0.1:2345:2345' 48 | build: 49 | context: . 50 | dockerfile: Dockerfile.dev 51 | volumes: 52 | - $PWD:/app:cached 53 | restart: on-failure 54 | entrypoint: /bin/zsh 55 | stdin_open: true 56 | tty: true 57 | networks: ['app-network'] 58 | 59 | networks: 60 | app-network: 61 | driver: bridge 62 | -------------------------------------------------------------------------------- /apps/server/src/repository/stats_repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bananocoin/boompow/apps/server/graph/model" 7 | "github.com/bananocoin/boompow/apps/server/src/database" 8 | "github.com/bananocoin/boompow/apps/server/src/models" 9 | "k8s.io/klog/v2" 10 | ) 11 | 12 | func UpdateStats(paymentRepo PaymentRepo, workRepo WorkRepo) error { 13 | // Connected clients 14 | nConnectedClients, err := database.GetRedisDB().GetNumberConnectedClients() 15 | if err != nil { 16 | klog.Infof("Error retrieving connected clients for stats sub %v", err) 17 | return err 18 | } 19 | // Services 20 | services, err := workRepo.GetServiceStats() 21 | if err != nil { 22 | klog.Infof("Error retrieving services for stats sub %v", err) 23 | return err 24 | } 25 | var serviceStats []*model.StatsServiceType 26 | for _, service := range services { 27 | serviceStats = append(serviceStats, &model.StatsServiceType{ 28 | Name: service.ServiceName, 29 | Website: service.ServiceWebsite, 30 | Requests: service.TotalRequests, 31 | }) 32 | } 33 | // Top 10 34 | top10, err := workRepo.GetTopContributors(100) 35 | if err != nil { 36 | klog.Infof("Error retrieving # services for stats sub %v", err) 37 | return err 38 | } 39 | var top10Contributors []*model.StatsUserType 40 | for _, u := range top10 { 41 | top10Contributors = append(top10Contributors, &model.StatsUserType{ 42 | BanAddress: u.BanAddress, 43 | TotalPaidBanano: u.TotalBan, 44 | }) 45 | } 46 | // Total paid 47 | totalPaidBan, err := paymentRepo.GetTotalPaidBanano() 48 | models.GetStatsInstance().Stats = &model.Stats{ConnectedWorkers: int(nConnectedClients), TotalPaidBanano: fmt.Sprintf("%.2f", totalPaidBan), RegisteredServiceCount: len(services), Top10: top10Contributors, Services: serviceStats} 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /services/moneybags/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bananocoin/boompow/services/moneybags 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/bananocoin/boompow/apps/server v0.0.0-20220810030035-6f62a2705842 7 | github.com/bananocoin/boompow/libs/models v0.0.0-20220810030035-6f62a2705842 8 | github.com/bananocoin/boompow/libs/utils v0.0.0-20220810030035-6f62a2705842 9 | github.com/google/uuid v1.3.0 10 | github.com/joho/godotenv v1.4.0 11 | gorm.io/gorm v1.23.8 12 | k8s.io/klog/v2 v2.70.1 13 | ) 14 | 15 | require ( 16 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect 17 | github.com/alicebob/miniredis/v2 v2.22.0 // indirect 18 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 19 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 20 | github.com/go-logr/logr v1.2.0 // indirect 21 | github.com/go-redis/redis/v9 v9.0.0-beta.2 // indirect 22 | github.com/golang-jwt/jwt/v4 v4.4.2 // indirect 23 | github.com/golang/glog v1.0.0 // indirect 24 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 25 | github.com/jackc/pgconn v1.13.0 // indirect 26 | github.com/jackc/pgio v1.0.0 // indirect 27 | github.com/jackc/pgpassfile v1.0.0 // indirect 28 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 29 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 30 | github.com/jackc/pgtype v1.12.0 // indirect 31 | github.com/jackc/pgx/v4 v4.17.0 // indirect 32 | github.com/jinzhu/inflection v1.0.0 // indirect 33 | github.com/jinzhu/now v1.1.5 // indirect 34 | github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect 35 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 36 | golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 // indirect 37 | golang.org/x/text v0.3.7 // indirect 38 | gorm.io/driver/postgres v1.3.8 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /kubernetes/moneybags/rpc_cron.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: CronJob 3 | metadata: 4 | name: moneybags-rpc-send 5 | namespace: boompow-next 6 | spec: 7 | schedule: "0 */3 * * *" 8 | jobTemplate: 9 | spec: 10 | template: 11 | spec: 12 | containers: 13 | - name: boompow-payments 14 | image: replaceme 15 | command: ["/bin/sh", "-c"] 16 | args: ["moneybags -rpc-send"] 17 | env: 18 | - name: DB_HOST 19 | value: pg-boompow.boompow-next 20 | - name: DB_PORT 21 | value: "5432" 22 | - name: DB_SSLMODE 23 | value: disable 24 | - name: DB_NAME 25 | value: postgres 26 | - name: DB_USER 27 | value: postgres 28 | - name: DB_PASS 29 | valueFrom: 30 | secretKeyRef: 31 | name: boompow 32 | key: db_password 33 | - name: REDIS_HOST 34 | value: redis.redis 35 | - name: REDIS_DB 36 | value: "18" 37 | - name: BPOW_WALLET_ID 38 | valueFrom: 39 | secretKeyRef: 40 | name: boompow 41 | key: wallet_id 42 | - name: BPOW_PRIZE_POOL 43 | valueFrom: 44 | secretKeyRef: 45 | name: boompow 46 | key: prize_pool 47 | - name: BPOW_WALLET_ADDRESS 48 | value: ban_1boompow14irck1yauquqypt7afqrh8b6bbu5r93pc6hgbqs7z6o99frcuym 49 | - name: RPC_URL 50 | value: http://pippin-banano.pippin:11338 51 | - name: ENVIRONMENT 52 | value: production 53 | restartPolicy: OnFailure 54 | -------------------------------------------------------------------------------- /apps/client/gql/client.go: -------------------------------------------------------------------------------- 1 | package gql 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/Khan/genqlient/graphql" 10 | ) 11 | 12 | type GQLError string 13 | 14 | const ( 15 | InvalidUsernamePasssword GQLError = "Invalid username or password" 16 | ServerError = "Unknown server error, try again later" 17 | ) 18 | 19 | var client graphql.Client 20 | 21 | type authedTransport struct { 22 | wrapped http.RoundTripper 23 | token string 24 | } 25 | 26 | func (t *authedTransport) RoundTrip(req *http.Request) (*http.Response, error) { 27 | req.Header.Set("Authorization", t.token) 28 | return t.wrapped.RoundTrip(req) 29 | } 30 | 31 | func InitGQLClient(url string) { 32 | client = graphql.NewClient(url, http.DefaultClient) 33 | } 34 | 35 | func InitGQLClientWithToken(url string, token string) { 36 | client = graphql.NewClient(url, &http.Client{Transport: &authedTransport{wrapped: http.DefaultTransport, token: token}}) 37 | } 38 | 39 | func Login(ctx context.Context, email string, password string) (*loginUserResponse, GQLError) { 40 | resp, err := loginUser(ctx, client, LoginInput{ 41 | Email: email, 42 | Password: password, 43 | }) 44 | 45 | if err != nil { 46 | fmt.Printf("Error logging in %v", err) 47 | if strings.Contains(err.Error(), "invalid email or password") { 48 | return nil, InvalidUsernamePasssword 49 | } 50 | return nil, ServerError 51 | } 52 | 53 | return resp, "" 54 | } 55 | 56 | func RefreshToken(ctx context.Context, token string) (string, error) { 57 | resp, err := refreshToken(ctx, client, RefreshTokenInput{ 58 | Token: token, 59 | }) 60 | 61 | if err != nil { 62 | fmt.Printf("\nError refreshing authentication token! You may need to restart the client and re-login %v", err) 63 | return "", err 64 | } 65 | fmt.Printf("\n👮 Refreshed authentication token") 66 | 67 | return resp.RefreshToken, nil 68 | } 69 | -------------------------------------------------------------------------------- /services/moneybags/rpcClient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/bananocoin/boompow/libs/models" 12 | "k8s.io/klog/v2" 13 | ) 14 | 15 | type RPCClient struct { 16 | Url string 17 | httpClient *http.Client 18 | } 19 | 20 | func NewRPCClient(url string) *RPCClient { 21 | return &RPCClient{ 22 | Url: url, 23 | httpClient: &http.Client{ 24 | Timeout: time.Second * 30, // Set a timeout for all requests 25 | }, 26 | } 27 | } 28 | 29 | type SendResponse struct { 30 | Block string `json:"block"` 31 | } 32 | 33 | // Base request 34 | func (client RPCClient) makeRequest(request interface{}) ([]byte, error) { 35 | requestBody, _ := json.Marshal(request) 36 | // HTTP post 37 | resp, err := client.httpClient.Post(client.Url, "application/json", bytes.NewBuffer(requestBody)) 38 | if err != nil { 39 | klog.Errorf("Error making RPC request %s", err) 40 | return nil, err 41 | } 42 | defer resp.Body.Close() 43 | // Try to decode+deserialize 44 | body, err := io.ReadAll(resp.Body) 45 | if err != nil { 46 | klog.Errorf("Error decoding response body %s", err) 47 | return nil, err 48 | } 49 | 50 | if resp.StatusCode != http.StatusOK { 51 | klog.Errorf("Received non-200 response: %s", body) 52 | return nil, errors.New("non-200 response received") 53 | } 54 | 55 | return body, nil 56 | } 57 | 58 | // send 59 | func (client RPCClient) MakeSendRequest(request models.SendRequest) (*SendResponse, error) { 60 | response, err := client.makeRequest(request) 61 | if err != nil { 62 | klog.Errorf("Error making request %s", err) 63 | return nil, err 64 | } 65 | // Try to decode+deserialize 66 | var sendResponse SendResponse 67 | err = json.Unmarshal(response, &sendResponse) 68 | if err != nil { 69 | klog.Errorf("Error unmarshaling response %s, %s", string(response), err) 70 | return nil, errors.New("Error") 71 | } 72 | return &sendResponse, nil 73 | } 74 | -------------------------------------------------------------------------------- /libs/utils/number/banano_test.go: -------------------------------------------------------------------------------- 1 | package number 2 | 3 | import "testing" 4 | 5 | func TestRawToBanano(t *testing.T) { 6 | // 1 7 | raw := "100000000000000000000000000000" 8 | expected := 1.0 9 | converted, _ := RawToBanano(raw, true) 10 | if converted != expected { 11 | t.Errorf("Expected %f but got %f", expected, converted) 12 | } 13 | // 1.01 14 | raw = "101000000000000000000000000000" 15 | expected = 1.01 16 | converted, _ = RawToBanano(raw, true) 17 | if converted != expected { 18 | t.Errorf("Expected %f but got %f", expected, converted) 19 | } 20 | // 1.019 21 | raw = "101900000000000000000000000000" 22 | expected = 1.01 23 | converted, _ = RawToBanano(raw, true) 24 | if converted != expected { 25 | t.Errorf("Expected %f but got %f", expected, converted) 26 | } 27 | // 100000 28 | raw = "10000000000000000000000000000000000" 29 | expected = 100000 30 | converted, _ = RawToBanano(raw, true) 31 | if converted != expected { 32 | t.Errorf("Expected %f but got %f", expected, converted) 33 | } 34 | // Error 35 | raw = "1234NotANumber" 36 | expected = 1234.123456 37 | _, err := RawToBanano(raw, true) 38 | if err == nil { 39 | t.Errorf("Expected error converting %s but didn't get one", raw) 40 | } 41 | } 42 | 43 | func TestBananoToRaw(t *testing.T) { 44 | // 1 45 | expected := "100000000000000000000000000000" 46 | amount := 1.0 47 | converted := BananoToRaw(amount) 48 | if converted != expected { 49 | t.Errorf("Expected %s but got %s", expected, converted) 50 | } 51 | // 1.01 52 | expected = "101000000000000000000000000000" 53 | amount = 1.01 54 | converted = BananoToRaw(amount) 55 | if converted != expected { 56 | t.Errorf("Expected %s but got %s", expected, converted) 57 | } 58 | // 100000 59 | expected = "10000000000000000000000000000000000" 60 | amount = 100000 61 | converted = BananoToRaw(amount) 62 | if converted != expected { 63 | t.Errorf("Expected %s but got %s", expected, converted) 64 | } 65 | } 66 | 67 | // 100000000000000000000000000000 68 | // 10000000000000000000000000000000 69 | -------------------------------------------------------------------------------- /apps/server/src/database/postgres.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bananocoin/boompow/apps/server/src/models" 7 | "gorm.io/driver/postgres" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type Config struct { 12 | Host string 13 | Port string 14 | Password string 15 | User string 16 | DBName string 17 | SSLMode string 18 | } 19 | 20 | func NewConnection(config *Config) (*gorm.DB, error) { 21 | dsn := fmt.Sprintf( 22 | "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", 23 | config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode, 24 | ) 25 | db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) 26 | if err != nil { 27 | return db, err 28 | } 29 | return db, nil 30 | } 31 | 32 | func DropAndCreateTables(db *gorm.DB) error { 33 | err := db.Migrator().DropTable(&models.User{}, &models.WorkResult{}, &models.Payment{}) 34 | if err != nil { 35 | return err 36 | } 37 | err = db.Exec(fmt.Sprintf("DROP TYPE IF EXISTS %s", models.PG_USER_TYPE_NAME)).Error 38 | if err != nil { 39 | return err 40 | } 41 | err = createTypes(db) 42 | if err != nil { 43 | return err 44 | } 45 | err = db.Migrator().CreateTable(&models.User{}, &models.WorkResult{}, &models.Payment{}) 46 | return err 47 | } 48 | 49 | func Migrate(db *gorm.DB) error { 50 | createTypes(db) 51 | return db.AutoMigrate(&models.User{}, &models.WorkResult{}, &models.Payment{}) 52 | } 53 | 54 | // Create types in postgres 55 | func createTypes(db *gorm.DB) error { 56 | result := db.Exec(fmt.Sprintf("SELECT 1 FROM pg_type WHERE typname = '%s';", models.PG_USER_TYPE_NAME)) 57 | 58 | switch { 59 | case result.RowsAffected == 0: 60 | if err := db.Exec(fmt.Sprintf("CREATE TYPE %s AS ENUM ('%s', '%s');", models.PG_USER_TYPE_NAME, models.PROVIDER, models.REQUESTER)).Error; err != nil { 61 | fmt.Printf("Error creating %s ENUM", models.PG_USER_TYPE_NAME) 62 | return err 63 | } 64 | 65 | return nil 66 | case result.Error != nil: 67 | return result.Error 68 | 69 | default: 70 | return nil 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /libs/utils/env.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "strings" 7 | 8 | "k8s.io/klog/v2" 9 | ) 10 | 11 | func GetEnv(key string, fallback string) string { 12 | value := os.Getenv(key) 13 | if len(value) == 0 { 14 | return fallback 15 | } 16 | return value 17 | } 18 | 19 | func GetBannedRewards() []string { 20 | raw := GetEnv("BPOW_BANNED_REWARDS", "") 21 | return strings.Split(raw, ",") 22 | } 23 | 24 | func GetAllowedEmails() []string { 25 | raw := GetEnv("BPOW_ALLOWED_EMAILS", "") 26 | return strings.Split(raw, ",") 27 | } 28 | 29 | func GetServiceTokens() []string { 30 | raw := GetEnv("BPOW_SERVICE_TOKENS", "") 31 | return strings.Split(raw, ",") 32 | } 33 | 34 | func GetJwtKey() []byte { 35 | privKey := GetEnv("PRIV_KEY", "badKey") 36 | if privKey == "badKey" { 37 | klog.Warningf("!!! DEFAULT JWT SIGNING KEY IS BEING USED, NOT SECURE !!!") 38 | } 39 | return []byte(privKey) 40 | } 41 | 42 | type SmtpConnInformation struct { 43 | Server string 44 | Port int 45 | Username string 46 | Password string 47 | } 48 | 49 | func GetSmtpConnInformation() *SmtpConnInformation { 50 | server := GetEnv("SMTP_SERVER", "") 51 | portRaw := GetEnv("SMTP_PORT", "-1") 52 | port, err := strconv.Atoi(portRaw) 53 | if err != nil { 54 | port = -1 55 | } 56 | username := GetEnv("SMTP_USERNAME", "") 57 | password := GetEnv("SMTP_PASSWORD", "") 58 | if server == "" || username == "" || password == "" || port == -1 { 59 | return nil 60 | } 61 | return &SmtpConnInformation{ 62 | Server: server, 63 | Port: port, 64 | Username: username, 65 | Password: password, 66 | } 67 | } 68 | 69 | func GetTotalPrizePool() int { 70 | totalPrizeRaw := GetEnv("BPOW_PRIZE_POOL", "1000") 71 | totalPrize, err := strconv.Atoi(totalPrizeRaw) 72 | if err != nil { 73 | totalPrize = 0 74 | } 75 | return totalPrize 76 | } 77 | 78 | func GetWalletID() string { 79 | return GetEnv("BPOW_WALLET_ID", "wallet_id_not_set") 80 | } 81 | 82 | func GetWalletAddress() string { 83 | return GetEnv("BPOW_WALLET_ADDRESS", "wallet_address_not_set") 84 | } 85 | -------------------------------------------------------------------------------- /apps/client/work/nanowork.go: -------------------------------------------------------------------------------- 1 | package work 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "fmt" 7 | "runtime" 8 | 9 | "github.com/Inkeliz/go-opencl/opencl" 10 | serializableModels "github.com/bananocoin/boompow/libs/models" 11 | "github.com/bananocoin/boompow/libs/utils/validation" 12 | "github.com/bbedward/nanopow" 13 | "k8s.io/klog/v2" 14 | ) 15 | 16 | type WorkPool struct { 17 | Pool *nanopow.Pool 18 | } 19 | 20 | func NewWorkPool(gpuOnly bool, devices []opencl.Device) *WorkPool { 21 | pool := nanopow.NewPool() 22 | for _, device := range devices { 23 | gpu, gpuErr := nanopow.NewWorkerGPU(device) 24 | if gpuErr == nil { 25 | pool.Workers = append(pool.Workers, gpu) 26 | } else { 27 | fmt.Printf("\n⚠️ Unable to use GPU %v", gpuErr) 28 | } 29 | } 30 | 31 | if gpuOnly && len(pool.Workers) == 0 { 32 | panic("Unable to initialize any GPUs, but gpu-only was set") 33 | } else if len(pool.Workers) == 0 { 34 | fmt.Printf("\n⚠️ Unable to initialize any GPUs, using CPU") 35 | } 36 | 37 | if !gpuOnly { 38 | threads := runtime.NumCPU() 39 | cpu, cpuErr := nanopow.NewWorkerCPUThread(uint64(threads)) 40 | if cpuErr == nil { 41 | pool.Workers = append(pool.Workers, cpu) 42 | } else { 43 | panic(fmt.Sprintf("Unable to initialize work pool for CPU %v", cpuErr)) 44 | } 45 | } 46 | 47 | return &WorkPool{ 48 | Pool: pool, 49 | } 50 | } 51 | 52 | func (p *WorkPool) WorkGenerate(item *serializableModels.ClientMessage) (string, error) { 53 | decoded, err := hex.DecodeString(item.Hash) 54 | if err != nil { 55 | return "", err 56 | } 57 | work, err := p.Pool.GenerateWork(decoded, validation.CalculateDifficulty(int64(item.DifficultyMultiplier))) 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | if !nanopow.IsValid(decoded, validation.CalculateDifficulty(int64(item.DifficultyMultiplier)), work) { 63 | klog.Errorf("\n⚠️ Generated invalid work for %s", item.Hash) 64 | return "", errors.New("Invalid work") 65 | } 66 | return WorkToString(work), nil 67 | } 68 | 69 | func WorkToString(w nanopow.Work) string { 70 | n := make([]byte, 8) 71 | copy(n, w[:]) 72 | 73 | return hex.EncodeToString(n) 74 | } 75 | -------------------------------------------------------------------------------- /apps/server/gqlgen.yml: -------------------------------------------------------------------------------- 1 | # Where are all the schema files located? globs are supported eg src/**/*.graphqls 2 | schema: 3 | - graph/*.graphqls 4 | 5 | # Where should the generated server code go? 6 | exec: 7 | filename: graph/generated/generated.go 8 | package: generated 9 | 10 | # Uncomment to enable federation 11 | # federation: 12 | # filename: graph/generated/federation.go 13 | # package: generated 14 | 15 | # Where should any generated models go? 16 | model: 17 | filename: graph/model/models_gen.go 18 | package: model 19 | 20 | # Where should the resolver implementations go? 21 | resolver: 22 | layout: follow-schema 23 | dir: graph 24 | package: graph 25 | 26 | # Optional: turn on use ` + "`" + `gqlgen:"fieldName"` + "`" + ` tags in your models 27 | # struct_tag: json 28 | 29 | # Optional: turn on to use []Thing instead of []*Thing 30 | # omit_slice_element_pointers: false 31 | 32 | # Optional: turn off to make struct-type struct fields not use pointers 33 | # e.g. type Thing struct { FieldA OtherThing } instead of { FieldA *OtherThing } 34 | # struct_fields_always_pointers: true 35 | 36 | # Optional: turn off to make resolvers return values instead of pointers for structs 37 | # resolvers_always_return_pointers: true 38 | 39 | # Optional: set to speed up generation time by not performing a final validation pass. 40 | # skip_validation: true 41 | 42 | # gqlgen will search for any type names in the schema in these go packages 43 | # if they match it will use them, otherwise it will generate them. 44 | autobind: 45 | # - "github.com/bananocoin/boompow/apps/server/graph/model" 46 | 47 | # This section declares type mapping between the GraphQL and go type systems 48 | # 49 | # The first line in each type will be used as defaults for resolver arguments and 50 | # modelgen, the others will be allowed when binding to fields. Configure them to 51 | # your liking 52 | models: 53 | ID: 54 | model: 55 | - github.com/99designs/gqlgen/graphql.ID 56 | - github.com/99designs/gqlgen/graphql.Int 57 | - github.com/99designs/gqlgen/graphql.Int64 58 | - github.com/99designs/gqlgen/graphql.Int32 59 | Int: 60 | model: 61 | - github.com/99designs/gqlgen/graphql.Int 62 | - github.com/99designs/gqlgen/graphql.Int64 63 | - github.com/99designs/gqlgen/graphql.Int32 64 | -------------------------------------------------------------------------------- /apps/server/src/models/sync_array.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type ActiveChannelObject struct { 8 | BlockAward bool 9 | RequesterEmail string 10 | RequestID string 11 | Hash string 12 | DifficultyMultiplier int 13 | Precache bool 14 | Chan chan []byte 15 | } 16 | 17 | // SyncArray builds an thread-safe array with some handy methods 18 | type SyncArray struct { 19 | mu sync.Mutex 20 | channels []*ActiveChannelObject 21 | } 22 | 23 | func NewSyncArray() *SyncArray { 24 | return &SyncArray{ 25 | channels: []*ActiveChannelObject{}, 26 | } 27 | } 28 | 29 | // See if element exists 30 | func (r *SyncArray) Exists(requestID string) bool { 31 | for _, v := range r.channels { 32 | if v.RequestID == requestID { 33 | return true 34 | } 35 | } 36 | return false 37 | } 38 | 39 | func (r *SyncArray) HashExists(hash string) bool { 40 | for _, v := range r.channels { 41 | if v.Hash == hash { 42 | return true 43 | } 44 | } 45 | return false 46 | } 47 | 48 | // Put value into map - synchronized 49 | func (r *SyncArray) Put(value *ActiveChannelObject) { 50 | r.mu.Lock() 51 | defer r.mu.Unlock() 52 | if !r.Exists(value.RequestID) { 53 | r.channels = append(r.channels, value) 54 | } 55 | } 56 | 57 | // Gets a value from the map - synchronized 58 | func (r *SyncArray) Get(requestID string) *ActiveChannelObject { 59 | r.mu.Lock() 60 | defer r.mu.Unlock() 61 | for _, v := range r.channels { 62 | if v.RequestID == requestID { 63 | return v 64 | } 65 | } 66 | 67 | return nil 68 | } 69 | 70 | // Removes specified hash - synchronized 71 | func (r *SyncArray) Delete(requestID string) { 72 | r.mu.Lock() 73 | defer r.mu.Unlock() 74 | index := r.IndexOf(requestID) 75 | if index > -1 { 76 | r.channels = remove(r.channels, r.IndexOf(requestID)) 77 | } 78 | } 79 | 80 | func (r *SyncArray) IndexOf(requestID string) int { 81 | for i, v := range r.channels { 82 | if v.RequestID == requestID { 83 | return i 84 | } 85 | } 86 | return -1 87 | } 88 | 89 | func (r *SyncArray) Len() int { 90 | r.mu.Lock() 91 | defer r.mu.Unlock() 92 | return len(r.channels) 93 | } 94 | 95 | // NOT thread safe, must be called from within a locked section 96 | func remove(s []*ActiveChannelObject, i int) []*ActiveChannelObject { 97 | s[i] = s[len(s)-1] 98 | return s[:len(s)-1] 99 | } 100 | -------------------------------------------------------------------------------- /apps/server/src/repository/payment_repo.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/bananocoin/boompow/apps/server/src/models" 5 | serializableModels "github.com/bananocoin/boompow/libs/models" 6 | "github.com/bananocoin/boompow/libs/utils/number" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type PaymentRepo interface { 11 | BatchCreateSendRequests(tx *gorm.DB, sendRequests []serializableModels.SendRequest) error 12 | GetPendingPayments(tx *gorm.DB) ([]serializableModels.SendRequest, error) 13 | SetBlockHash(tx *gorm.DB, sendId string, blockHash string) error 14 | GetTotalPaidBanano() (float64, error) 15 | } 16 | 17 | type PaymentService struct { 18 | Db *gorm.DB 19 | } 20 | 21 | var _ PaymentRepo = &PaymentService{} 22 | 23 | func NewPaymentService(db *gorm.DB) *PaymentService { 24 | return &PaymentService{ 25 | Db: db, 26 | } 27 | } 28 | 29 | // Create payments in database 30 | func (s *PaymentService) BatchCreateSendRequests(tx *gorm.DB, sendRequests []serializableModels.SendRequest) error { 31 | payments := make([]models.Payment, len(sendRequests)) 32 | 33 | for i, sendRequest := range sendRequests { 34 | payments[i] = models.Payment{ 35 | SendId: sendRequest.ID, 36 | SendJson: sendRequest, 37 | PaidTo: sendRequest.PaidTo, 38 | } 39 | } 40 | 41 | return tx.Create(&payments).Error 42 | } 43 | 44 | // Get all payments with null block hash 45 | func (s *PaymentService) GetPendingPayments(tx *gorm.DB) ([]serializableModels.SendRequest, error) { 46 | var res []serializableModels.SendRequest 47 | 48 | if err := tx.Model(&models.Payment{}).Select("send_json").Where("block_hash is null").Find(&res).Error; err != nil { 49 | return nil, err 50 | } 51 | 52 | return res, nil 53 | } 54 | 55 | // Update payment with block hash 56 | func (s *PaymentService) SetBlockHash(tx *gorm.DB, sendId string, blockHash string) error { 57 | return tx.Model(&models.Payment{}).Where("send_id = ?", sendId).Update("block_hash", blockHash).Error 58 | } 59 | 60 | // Get total paid sum 61 | func (s *PaymentService) GetTotalPaidBanano() (float64, error) { 62 | var totalPaid string 63 | if err := s.Db.Model(&models.Payment{}).Select("sum(cast(send_json->>'amount'as numeric)) as total_raw").Find(&totalPaid).Error; err != nil { 64 | return -1, err 65 | } 66 | asBan, err := number.RawToBanano(totalPaid, true) 67 | if err != nil { 68 | return -1, err 69 | } 70 | 71 | // Magic number, this is what BPoW v1 paid so add it to the total 72 | asBan += 1728016 73 | 74 | return asBan, nil 75 | } 76 | -------------------------------------------------------------------------------- /apps/server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bananocoin/boompow/apps/server 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.17.20 7 | github.com/alicebob/miniredis/v2 v2.23.0 8 | github.com/bananocoin/boompow/libs/models v0.0.0-20221028000758-87e26bd468df 9 | github.com/bananocoin/boompow/libs/utils v0.0.0-20221028000758-87e26bd468df 10 | github.com/bitfield/script v0.20.2 11 | github.com/go-chi/chi/v5 v5.0.7 12 | github.com/go-chi/cors v1.2.1 13 | github.com/go-chi/httprate v0.7.0 14 | github.com/go-co-op/gocron v1.25.0 15 | github.com/go-redis/redis/v9 v9.0.0-rc.1 16 | github.com/google/uuid v1.3.0 17 | github.com/gorilla/websocket v1.5.0 18 | github.com/jackc/pgconn v1.13.0 19 | github.com/joho/godotenv v1.4.0 20 | github.com/vektah/gqlparser/v2 v2.5.1 21 | golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 22 | gorm.io/driver/postgres v1.3.9 23 | gorm.io/gorm v1.24.1 24 | k8s.io/klog/v2 v2.80.1 25 | ) 26 | 27 | require ( 28 | github.com/jpillora/backoff v1.0.0 // indirect 29 | github.com/robfig/cron/v3 v3.0.1 // indirect 30 | ) 31 | 32 | require ( 33 | bitbucket.org/creachadair/shell v0.0.7 // indirect 34 | github.com/agnivade/levenshtein v1.1.1 // indirect 35 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect 36 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 37 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 38 | github.com/go-logr/logr v1.2.3 // indirect 39 | github.com/golang-jwt/jwt/v4 v4.4.2 // indirect 40 | github.com/hashicorp/golang-lru v0.5.4 // indirect 41 | github.com/itchyny/gojq v0.12.9 // indirect 42 | github.com/itchyny/timefmt-go v0.1.4 // indirect 43 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 44 | github.com/jackc/pgio v1.0.0 // indirect 45 | github.com/jackc/pgpassfile v1.0.0 // indirect 46 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 47 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 48 | github.com/jackc/pgtype v1.12.0 // indirect 49 | github.com/jackc/pgx/v4 v4.17.2 // indirect 50 | github.com/jinzhu/inflection v1.0.0 // indirect 51 | github.com/jinzhu/now v1.1.5 // indirect 52 | github.com/lib/pq v1.10.6 // indirect 53 | github.com/mitchellh/mapstructure v1.5.0 // indirect 54 | github.com/recws-org/recws v1.4.0 55 | github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect 56 | golang.org/x/crypto v0.1.0 // indirect 57 | golang.org/x/sys v0.1.0 // indirect 58 | golang.org/x/text v0.4.0 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /kubernetes/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: boompow-ingress 5 | namespace: boompow-next 6 | annotations: 7 | kubernetes.io/ingress.class: "nginx" 8 | cert-manager.io/cluster-issuer: "letsencrypt-prod" 9 | nginx.ingress.kubernetes.io/configuration-snippet: | 10 | real_ip_header CF-Connecting-IP; 11 | # Run ~/scripts/cf_ips.sh to update this list 12 | # https://www.cloudflare.com/ips 13 | # IPv4 14 | allow 173.245.48.0/20; 15 | allow 103.21.244.0/22; 16 | allow 103.22.200.0/22; 17 | allow 103.31.4.0/22; 18 | allow 141.101.64.0/18; 19 | allow 108.162.192.0/18; 20 | allow 190.93.240.0/20; 21 | allow 188.114.96.0/20; 22 | allow 197.234.240.0/22; 23 | allow 198.41.128.0/17; 24 | allow 162.158.0.0/15; 25 | allow 104.16.0.0/13; 26 | allow 104.24.0.0/14; 27 | allow 172.64.0.0/13; 28 | allow 131.0.72.0/22; 29 | # IPv6 30 | allow 2400:cb00::/32; 31 | allow 2606:4700::/32; 32 | allow 2803:f800::/32; 33 | allow 2405:b500::/32; 34 | allow 2405:8100::/32; 35 | allow 2a06:98c0::/29; 36 | allow 2c0f:f248::/32; 37 | # Generated at Tue Aug 23 12:06:49 EDT 2022 38 | deny all; # deny all remaining ips 39 | nginx.ingress.kubernetes.io/add-base-url: "true" 40 | nginx.ingress.kubernetes.io/ssl-redirect: "true" 41 | nginx.ingress.kubernetes.io/websocket-services: "boompow-service" 42 | nginx.ingress.kubernetes.io/proxy-send-timeout: "1800" 43 | nginx.ingress.kubernetes.io/proxy-read-timeout: "1800" 44 | nginx.ingress.kubernetes.io/upstream-hash-by: $remote_addr 45 | nginx.ingress.kubernetes.io/affinity: "cookie" 46 | nginx.ingress.kubernetes.io/session-cookie-name: "boompow_socket" 47 | nginx.ingress.kubernetes.io/session-cookie-expires: "172800" 48 | nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" 49 | spec: 50 | tls: 51 | - hosts: 52 | - boompow.banano.cc 53 | secretName: boompow-banano-cc-server-secret 54 | rules: 55 | - host: boompow.banano.cc 56 | http: 57 | paths: 58 | - path: /graphql 59 | pathType: Prefix 60 | backend: 61 | service: 62 | name: boompow-service 63 | port: 64 | number: 8080 65 | - path: /ws 66 | pathType: Prefix 67 | backend: 68 | service: 69 | name: boompow-service 70 | port: 71 | number: 8080 -------------------------------------------------------------------------------- /apps/client/work/processor.go: -------------------------------------------------------------------------------- 1 | package work 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/Inkeliz/go-opencl/opencl" 10 | "github.com/bananocoin/boompow/apps/client/models" 11 | "github.com/bananocoin/boompow/apps/client/websocket" 12 | serializableModels "github.com/bananocoin/boompow/libs/models" 13 | ) 14 | 15 | type WorkProcessor struct { 16 | Queue *models.RandomAccessQueue 17 | // WorkQueueChan is where we write requests from the websocket 18 | WorkQueueChan chan *serializableModels.ClientMessage 19 | WSService *websocket.WebsocketService 20 | WorkPool *WorkPool 21 | mu sync.Mutex 22 | } 23 | 24 | func NewWorkProcessor(ws *websocket.WebsocketService, gpuOnly bool, devices []opencl.Device) *WorkProcessor { 25 | wp := NewWorkPool(gpuOnly, devices) 26 | return &WorkProcessor{ 27 | Queue: models.NewRandomAccessQueue(), 28 | WorkQueueChan: make(chan *serializableModels.ClientMessage, 100), 29 | WSService: ws, 30 | WorkPool: wp, 31 | } 32 | } 33 | 34 | // RequestQueueWorker - is a worker that receives work requests directly from the websocket, adds them to the queue, and determines what should be worked on next 35 | func (wp *WorkProcessor) StartRequestQueueWorker() { 36 | for range wp.WorkQueueChan { 37 | // Pop random unit of work from queue, begin computation 38 | workItem := wp.Queue.PopRandom() 39 | if workItem != nil { 40 | ctx, cancel := context.WithCancel(context.Background()) 41 | defer cancel() 42 | // Generate work with timeout 43 | ch := make(chan string) 44 | 45 | go func() { 46 | wp.mu.Lock() 47 | defer wp.mu.Unlock() 48 | result, err := wp.WorkPool.WorkGenerate(workItem) 49 | if err != nil { 50 | result = "" 51 | } 52 | 53 | select { 54 | default: 55 | ch <- result 56 | case <-ctx.Done(): 57 | } 58 | }() 59 | 60 | select { 61 | case result := <-ch: 62 | if result != "" { 63 | // Send result back to server 64 | clientWorkResult := serializableModels.ClientWorkResponse{ 65 | RequestID: workItem.RequestID, 66 | Hash: workItem.Hash, 67 | Result: result, 68 | } 69 | wp.WSService.WS.WriteJSON(clientWorkResult) 70 | } else { 71 | fmt.Printf("\n❌ Error: generate work for %s\n", workItem.Hash) 72 | } 73 | case <-time.After(10 * time.Second): 74 | fmt.Printf("\n❌ Error: took longer than 10s to generate work for %s", workItem.Hash) 75 | } 76 | } 77 | } 78 | } 79 | 80 | // Start both workers 81 | func (wp *WorkProcessor) StartAsync() { 82 | go wp.StartRequestQueueWorker() 83 | } 84 | -------------------------------------------------------------------------------- /libs/utils/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= 5 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 6 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 7 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 8 | github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= 9 | github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 14 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 15 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 16 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 17 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= 18 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 19 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= 20 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 25 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= 27 | k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 28 | k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= 29 | k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 30 | -------------------------------------------------------------------------------- /apps/client/models/random_access_queue.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | 7 | serializableModels "github.com/bananocoin/boompow/libs/models" 8 | ) 9 | 10 | // RandomAccessQueue provides a data struture that can be access randomly 11 | // This helps workers clears backlogs of work more evenly 12 | // If there is 20 items on 3 workers, each worker will access the next unit of work randomly 13 | type RandomAccessQueue struct { 14 | mu sync.Mutex 15 | hashes []serializableModels.ClientMessage 16 | } 17 | 18 | func NewRandomAccessQueue() *RandomAccessQueue { 19 | return &RandomAccessQueue{ 20 | hashes: []serializableModels.ClientMessage{}, 21 | } 22 | } 23 | 24 | // See if element exists 25 | func (r *RandomAccessQueue) exists(hash string) bool { 26 | for _, v := range r.hashes { 27 | if v.Hash == hash { 28 | return true 29 | } 30 | } 31 | return false 32 | } 33 | 34 | // Get length - synchronized 35 | func (r *RandomAccessQueue) Len() int { 36 | r.mu.Lock() 37 | defer r.mu.Unlock() 38 | return len(r.hashes) 39 | } 40 | 41 | // Put value into map - synchronized 42 | func (r *RandomAccessQueue) Put(value serializableModels.ClientMessage) { 43 | r.mu.Lock() 44 | defer r.mu.Unlock() 45 | if !r.exists(value.Hash) { 46 | r.hashes = append(r.hashes, value) 47 | } 48 | } 49 | 50 | // Removes and returns a random value from the map - synchronized 51 | func (r *RandomAccessQueue) PopRandom() *serializableModels.ClientMessage { 52 | r.mu.Lock() 53 | defer r.mu.Unlock() 54 | if len(r.hashes) == 0 { 55 | return nil 56 | } 57 | index := rand.Intn(len(r.hashes)) 58 | ret := r.hashes[index] 59 | r.hashes = remove(r.hashes, index) 60 | 61 | return &ret 62 | } 63 | 64 | // Gets a value from the map - synchronized 65 | func (r *RandomAccessQueue) Get(hash string) *serializableModels.ClientMessage { 66 | r.mu.Lock() 67 | defer r.mu.Unlock() 68 | if r.exists(hash) { 69 | return &r.hashes[r.indexOf(hash)] 70 | } 71 | 72 | return nil 73 | } 74 | 75 | // Removes specified hash - synchronized 76 | func (r *RandomAccessQueue) Delete(hash string) { 77 | r.mu.Lock() 78 | defer r.mu.Unlock() 79 | index := r.indexOf(hash) 80 | if index > -1 { 81 | r.hashes = remove(r.hashes, r.indexOf(hash)) 82 | } 83 | } 84 | 85 | func (r *RandomAccessQueue) indexOf(hash string) int { 86 | for i, v := range r.hashes { 87 | if v.Hash == hash { 88 | return i 89 | } 90 | } 91 | return -1 92 | } 93 | 94 | // NOT thread safe, must be called from within a locked section 95 | func remove(s []serializableModels.ClientMessage, i int) []serializableModels.ClientMessage { 96 | s[i] = s[len(s)-1] 97 | return s[:len(s)-1] 98 | } 99 | -------------------------------------------------------------------------------- /apps/server/src/database/redis_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | utils "github.com/bananocoin/boompow/libs/utils/testing" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // Test that it panics when we try to call Newconnection with mock=true and not testing db 12 | func TestMockRedis(t *testing.T) { 13 | os.Setenv("MOCK_REDIS", "true") 14 | 15 | redis := GetRedisDB() 16 | utils.AssertEqual(t, true, redis.Mock) 17 | } 18 | 19 | func TestRedis(t *testing.T) { 20 | os.Setenv("MOCK_REDIS", "true") 21 | 22 | redis := GetRedisDB() 23 | 24 | // Confirmation token bits 25 | if err := redis.SetConfirmationToken("email", "token"); err != nil { 26 | t.Errorf("Error setting confirmation token: %s", err) 27 | } 28 | token, err := redis.GetConfirmationToken("email") 29 | utils.AssertEqual(t, nil, err) 30 | utils.AssertEqual(t, "token", token) 31 | 32 | ret, err := redis.DeleteConfirmationToken("email") 33 | utils.AssertEqual(t, nil, err) 34 | utils.AssertEqual(t, int64(1), ret) 35 | 36 | // Connected clients bits 37 | if err := redis.AddConnectedClient("1"); err != nil { 38 | t.Errorf("Error adding client: %s", err) 39 | } 40 | ret, err = redis.GetNumberConnectedClients() 41 | utils.AssertEqual(t, nil, err) 42 | utils.AssertEqual(t, int64(1), ret) 43 | if err := redis.RemoveConnectedClient("1"); err != nil { 44 | t.Errorf("Error removing client: %s", err) 45 | } 46 | ret, err = redis.GetNumberConnectedClients() 47 | utils.AssertEqual(t, nil, err) 48 | utils.AssertEqual(t, int64(0), ret) 49 | // Add a couple clients 50 | if err := redis.AddConnectedClient("1"); err != nil { 51 | t.Errorf("Error adding client: %s", err) 52 | } 53 | if err := redis.AddConnectedClient("2"); err != nil { 54 | t.Errorf("Error adding client: %s", err) 55 | } 56 | ret, err = redis.GetNumberConnectedClients() 57 | utils.AssertEqual(t, nil, err) 58 | utils.AssertEqual(t, int64(2), ret) 59 | ret, err = redis.WipeAllConnectedClients() 60 | utils.AssertEqual(t, nil, err) 61 | ret, err = redis.GetNumberConnectedClients() 62 | utils.AssertEqual(t, nil, err) 63 | utils.AssertEqual(t, int64(0), ret) 64 | 65 | // Service token bits 66 | uid := uuid.New() 67 | if err := redis.AddServiceToken(uid, "token"); err != nil { 68 | t.Errorf("Error adding service token: %s", err) 69 | } 70 | uidStr, err := redis.GetServiceTokenUser("token") 71 | utils.AssertEqual(t, nil, err) 72 | utils.AssertEqual(t, uid.String(), uidStr) 73 | _, err = redis.GetServiceTokenUser("nonexistentoken") 74 | utils.AssertEqual(t, true, err != nil) 75 | 76 | tokenStr, err := redis.GetServiceTokenForUser(uid) 77 | utils.AssertEqual(t, nil, err) 78 | utils.AssertEqual(t, "token", tokenStr) 79 | } 80 | -------------------------------------------------------------------------------- /libs/utils/testing/main.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "path/filepath" 7 | "reflect" 8 | "runtime" 9 | "testing" 10 | "text/tabwriter" 11 | 12 | "k8s.io/klog/v2" 13 | ) 14 | 15 | // AssertEqual checks if values are equal 16 | func AssertEqual(t testing.TB, expected, actual interface{}, description ...string) { 17 | if reflect.DeepEqual(expected, actual) { 18 | return 19 | } 20 | 21 | var aType = "" 22 | var bType = "" 23 | 24 | if expected != nil { 25 | aType = reflect.TypeOf(expected).String() 26 | } 27 | if actual != nil { 28 | bType = reflect.TypeOf(actual).String() 29 | } 30 | 31 | testName := "AssertEqual" 32 | if t != nil { 33 | testName = t.Name() 34 | } 35 | 36 | _, file, line, _ := runtime.Caller(1) 37 | 38 | var buf bytes.Buffer 39 | w := tabwriter.NewWriter(&buf, 0, 0, 5, ' ', 0) 40 | fmt.Fprintf(w, "\nTest:\t%s", testName) 41 | fmt.Fprintf(w, "\nTrace:\t%s:%d", filepath.Base(file), line) 42 | if len(description) > 0 { 43 | fmt.Fprintf(w, "\nDescription:\t%s", description[0]) 44 | } 45 | fmt.Fprintf(w, "\nExpect:\t%v\t(%s)", expected, aType) 46 | fmt.Fprintf(w, "\nResult:\t%v\t(%s)", actual, bType) 47 | 48 | result := "" 49 | if err := w.Flush(); err != nil { 50 | result = err.Error() 51 | } else { 52 | result = buf.String() 53 | } 54 | 55 | if t != nil { 56 | t.Fatal(result) 57 | } else { 58 | klog.Fatal(result) 59 | } 60 | } 61 | 62 | // AssertNotEqual checks if values are not equal 63 | func AssertNotEqual(t testing.TB, expected, actual interface{}, description ...string) { 64 | if !reflect.DeepEqual(expected, actual) { 65 | return 66 | } 67 | 68 | var aType = "" 69 | var bType = "" 70 | 71 | if expected != nil { 72 | aType = reflect.TypeOf(expected).String() 73 | } 74 | if actual != nil { 75 | bType = reflect.TypeOf(actual).String() 76 | } 77 | 78 | testName := "AssertNotEqual" 79 | if t != nil { 80 | testName = t.Name() 81 | } 82 | 83 | _, file, line, _ := runtime.Caller(1) 84 | 85 | var buf bytes.Buffer 86 | w := tabwriter.NewWriter(&buf, 0, 0, 5, ' ', 0) 87 | fmt.Fprintf(w, "\nTest:\t%s", testName) 88 | fmt.Fprintf(w, "\nTrace:\t%s:%d", filepath.Base(file), line) 89 | if len(description) > 0 { 90 | fmt.Fprintf(w, "\nDescription:\t%s", description[0]) 91 | } 92 | fmt.Fprintf(w, "\nExpect:\t%v\t(%s)", expected, aType) 93 | fmt.Fprintf(w, "\nResult:\t%v\t(%s)", actual, bType) 94 | 95 | result := "" 96 | if err := w.Flush(); err != nil { 97 | result = err.Error() 98 | } else { 99 | result = buf.String() 100 | } 101 | 102 | if t != nil { 103 | t.Fatal(result) 104 | } else { 105 | klog.Fatal(result) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /apps/server/graph/schema.graphqls: -------------------------------------------------------------------------------- 1 | enum UserType { 2 | PROVIDER 3 | REQUESTER 4 | } 5 | 6 | type User { 7 | id: ID! 8 | email: String! 9 | createdAt: String! 10 | updatedAt: String! 11 | type: UserType! 12 | banAddress: String 13 | } 14 | 15 | type StatsUserType { 16 | banAddress: String! 17 | totalPaidBanano: String! 18 | } 19 | 20 | type StatsServiceType { 21 | name: String! 22 | website: String! 23 | requests: Int! 24 | } 25 | 26 | type Stats { 27 | connectedWorkers: Int! 28 | totalPaidBanano: String! 29 | registeredServiceCount: Int! 30 | top10: [StatsUserType]! 31 | services: [StatsServiceType]! 32 | } 33 | 34 | input RefreshTokenInput { 35 | token: String! 36 | } 37 | 38 | input VerifyEmailInput { 39 | email: String! 40 | token: String! 41 | } 42 | 43 | input VerifyServiceInput { 44 | email: String! 45 | token: String! 46 | } 47 | 48 | input UserInput { 49 | email: String! 50 | password: String! 51 | type: UserType! 52 | banAddress: String 53 | serviceName: String 54 | serviceWebsite: String 55 | } 56 | 57 | input LoginInput { 58 | email: String! 59 | password: String! 60 | } 61 | 62 | input WorkGenerateInput { 63 | hash: String! 64 | difficultyMultiplier: Int! 65 | blockAward: Boolean 66 | } 67 | 68 | input ResetPasswordInput { 69 | email: String! 70 | } 71 | 72 | input ResendConfirmationEmailInput { 73 | email: String! 74 | } 75 | 76 | type LoginResponse { 77 | token: String! 78 | email: String! 79 | type: UserType! 80 | banAddress: String 81 | serviceName: String 82 | serviceWebsite: String 83 | emailVerified: Boolean! 84 | } 85 | 86 | type GetUserResponse { 87 | email: String! 88 | type: UserType! 89 | banAddress: String 90 | serviceName: String 91 | serviceWebsite: String 92 | emailVerified: Boolean! 93 | canRequestWork: Boolean! 94 | } 95 | 96 | input ChangePasswordInput { 97 | newPassword: String! 98 | } 99 | 100 | type Mutation { 101 | # Related to user authentication and authorization 102 | createUser(input: UserInput!): User! 103 | login(input: LoginInput!): LoginResponse! 104 | refreshToken(input: RefreshTokenInput!): String! 105 | workGenerate(input: WorkGenerateInput!): String! 106 | generateOrGetServiceToken: String! 107 | resetPassword(input: ResetPasswordInput!): Boolean! 108 | resendConfirmationEmail(input: ResendConfirmationEmailInput!): Boolean! 109 | sendConfirmationEmail: Boolean! 110 | changePassword(input: ChangePasswordInput!): Boolean! 111 | } 112 | 113 | type Query { 114 | # User queries 115 | verifyEmail(input: VerifyEmailInput!): Boolean! 116 | verifyService(input: VerifyServiceInput!): Boolean! 117 | getUser: GetUserResponse! 118 | stats: Stats! 119 | } 120 | -------------------------------------------------------------------------------- /libs/utils/validation/banano.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "encoding/base32" 5 | "errors" 6 | "regexp" 7 | 8 | "github.com/bananocoin/boompow/libs/utils/ed25519" 9 | "golang.org/x/crypto/blake2b" 10 | ) 11 | 12 | // nano uses a non-standard base32 character set. 13 | const EncodeNano = "13456789abcdefghijkmnopqrstuwxyz" 14 | 15 | var NanoEncoding = base32.NewEncoding(EncodeNano) 16 | 17 | const bananoRegexStr = "(?:ban)(?:_)(?:1|3)(?:[13456789abcdefghijkmnopqrstuwxyz]{59})" 18 | 19 | var bananoRegex = regexp.MustCompile(bananoRegexStr) 20 | 21 | // ValidateAddress - Returns true if a banano address is valid 22 | func ValidateAddress(account string) bool { 23 | if !bananoRegex.MatchString(account) { 24 | return false 25 | } 26 | 27 | _, err := AddressToPub(account) 28 | if err != nil { 29 | return false 30 | } 31 | return true 32 | } 33 | 34 | // Convert address to a public key 35 | func AddressToPub(account string) (public_key []byte, err error) { 36 | address := string(account) 37 | 38 | if address[:4] == "xrb_" || address[:4] == "ban_" { 39 | address = address[4:] 40 | } else if address[:5] == "nano_" { 41 | address = address[5:] 42 | } else { 43 | return nil, errors.New("Invalid address format") 44 | } 45 | // A valid nano address is 64 bytes long 46 | // First 5 are simply a hard-coded string nano_ for ease of use 47 | // The following 52 characters form the address, and the final 48 | // 8 are a checksum. 49 | // They are base 32 encoded with a custom encoding. 50 | if len(address) == 60 { 51 | // The nano address string is 260bits which doesn't fall on a 52 | // byte boundary. pad with zeros to 280bits. 53 | // (zeros are encoded as 1 in nano's 32bit alphabet) 54 | key_b32nano := "1111" + address[0:52] 55 | input_checksum := address[52:] 56 | 57 | key_bytes, err := NanoEncoding.DecodeString(key_b32nano) 58 | if err != nil { 59 | return nil, err 60 | } 61 | // strip off upper 24 bits (3 bytes). 20 padding was added by us, 62 | // 4 is unused as account is 256 bits. 63 | key_bytes = key_bytes[3:] 64 | 65 | // nano checksum is calculated by hashing the key and reversing the bytes 66 | valid := NanoEncoding.EncodeToString(GetAddressChecksum(key_bytes)) == input_checksum 67 | if valid { 68 | return key_bytes, nil 69 | } else { 70 | return nil, errors.New("Invalid address checksum") 71 | } 72 | } 73 | 74 | return nil, errors.New("Invalid address format") 75 | } 76 | 77 | func GetAddressChecksum(pub ed25519.PublicKey) []byte { 78 | hash, err := blake2b.New(5, nil) 79 | if err != nil { 80 | panic("Unable to create hash") 81 | } 82 | 83 | hash.Write(pub) 84 | return Reversed(hash.Sum(nil)) 85 | } 86 | 87 | func Reversed(str []byte) (result []byte) { 88 | for i := len(str) - 1; i >= 0; i-- { 89 | result = append(result, str[i]) 90 | } 91 | return result 92 | } 93 | -------------------------------------------------------------------------------- /apps/server/src/tests/payment_repo_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/bananocoin/boompow/apps/server/src/database" 9 | "github.com/bananocoin/boompow/apps/server/src/repository" 10 | "github.com/bananocoin/boompow/libs/models" 11 | "github.com/bananocoin/boompow/libs/utils/number" 12 | utils "github.com/bananocoin/boompow/libs/utils/testing" 13 | ) 14 | 15 | // Test payment repo 16 | func TestPaymentrepo(t *testing.T) { 17 | os.Setenv("MOCK_REDIS", "true") 18 | mockDb, err := database.NewConnection(&database.Config{ 19 | Host: os.Getenv("DB_MOCK_HOST"), 20 | Port: os.Getenv("DB_MOCK_PORT"), 21 | Password: os.Getenv("DB_MOCK_PASS"), 22 | User: os.Getenv("DB_MOCK_USER"), 23 | SSLMode: os.Getenv("DB_SSLMODE"), 24 | DBName: "testing", 25 | }) 26 | utils.AssertEqual(t, nil, err) 27 | err = database.DropAndCreateTables(mockDb) 28 | utils.AssertEqual(t, nil, err) 29 | userRepo := repository.NewUserService(mockDb) 30 | paymentRepo := repository.NewPaymentService(mockDb) 31 | 32 | // Create some users 33 | err = userRepo.CreateMockUsers() 34 | utils.AssertEqual(t, nil, err) 35 | 36 | providerEmail := "provider@gmail.com" 37 | // Get user 38 | provider, _ := userRepo.GetUser(nil, &providerEmail) 39 | 40 | // Create some payments 41 | sendRequestsRaw := []models.SendRequest{} 42 | 43 | for i := 0; i < 3; i++ { 44 | sendRequestsRaw = append(sendRequestsRaw, models.SendRequest{ 45 | BaseRequest: models.SendAction, 46 | Wallet: strconv.FormatInt(int64(i), 10), 47 | Source: strconv.FormatInt(int64(i), 10), 48 | Destination: strconv.FormatInt(int64(i), 10), 49 | AmountRaw: number.BananoToRaw(float64(i)), 50 | // Just a unique payment identifier 51 | ID: strconv.FormatInt(int64(i), 10), 52 | PaidTo: provider.ID, 53 | }) 54 | } 55 | err = paymentRepo.BatchCreateSendRequests(mockDb, sendRequestsRaw) 56 | utils.AssertEqual(t, nil, err) 57 | 58 | // Get payments 59 | payments, err := paymentRepo.GetPendingPayments(mockDb) 60 | utils.AssertEqual(t, nil, err) 61 | utils.AssertEqual(t, 3, len(payments)) 62 | for p := range payments { 63 | utils.AssertEqual(t, sendRequestsRaw[p].ID, payments[p].ID) 64 | } 65 | 66 | // Update one payment with block hash 67 | err = paymentRepo.SetBlockHash(mockDb, "1", "1") 68 | utils.AssertEqual(t, nil, err) 69 | 70 | // Check that we don't get this payment in the unpaid query 71 | payments, err = paymentRepo.GetPendingPayments(mockDb) 72 | utils.AssertEqual(t, nil, err) 73 | utils.AssertEqual(t, 2, len(payments)) 74 | // Assert that we don't get payment ID "1" which has been paid 75 | for p := range payments { 76 | utils.AssertEqual(t, true, payments[p].ID == "0" || payments[p].ID == "2") 77 | } 78 | 79 | // Check out total paid 80 | totalPaid, err := paymentRepo.GetTotalPaidBanano() 81 | utils.AssertEqual(t, nil, err) 82 | utils.AssertEqual(t, 3.0+1728016, totalPaid) 83 | } 84 | -------------------------------------------------------------------------------- /libs/utils/ed25519/ed25519_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ed25519 6 | 7 | import ( 8 | "bytes" 9 | "crypto" 10 | "crypto/rand" 11 | "testing" 12 | 13 | "github.com/bananocoin/boompow/libs/utils/ed25519/edwards25519" 14 | ) 15 | 16 | type zeroReader struct{} 17 | 18 | func (zeroReader) Read(buf []byte) (int, error) { 19 | for i := range buf { 20 | buf[i] = 0 21 | } 22 | return len(buf), nil 23 | } 24 | 25 | func TestUnmarshalMarshal(t *testing.T) { 26 | pub, _, _ := GenerateKey(rand.Reader) 27 | 28 | var A edwards25519.ExtendedGroupElement 29 | var pubBytes [32]byte 30 | copy(pubBytes[:], pub) 31 | if !A.FromBytes(&pubBytes) { 32 | t.Fatalf("ExtendedGroupElement.FromBytes failed") 33 | } 34 | 35 | var pub2 [32]byte 36 | A.ToBytes(&pub2) 37 | 38 | if pubBytes != pub2 { 39 | t.Errorf("FromBytes(%v)->ToBytes does not round-trip, got %x\n", pubBytes, pub2) 40 | } 41 | } 42 | 43 | func TestSignVerify(t *testing.T) { 44 | var zero zeroReader 45 | public, private, _ := GenerateKey(zero) 46 | 47 | message := []byte("test message") 48 | sig := Sign(private, message) 49 | if !Verify(public, message, sig) { 50 | t.Errorf("valid signature rejected") 51 | } 52 | 53 | wrongMessage := []byte("wrong message") 54 | if Verify(public, wrongMessage, sig) { 55 | t.Errorf("signature of different message accepted") 56 | } 57 | } 58 | 59 | func TestCryptoSigner(t *testing.T) { 60 | var zero zeroReader 61 | public, private, _ := GenerateKey(zero) 62 | 63 | signer := crypto.Signer(private) 64 | 65 | publicInterface := signer.Public() 66 | public2, ok := publicInterface.(PublicKey) 67 | if !ok { 68 | t.Fatalf("expected PublicKey from Public() but got %T", publicInterface) 69 | } 70 | 71 | if !bytes.Equal(public, public2) { 72 | t.Errorf("public keys do not match: original:%x vs Public():%x", public, public2) 73 | } 74 | 75 | message := []byte("message") 76 | var noHash crypto.Hash 77 | signature, err := signer.Sign(zero, message, noHash) 78 | if err != nil { 79 | t.Fatalf("error from Sign(): %s", err) 80 | } 81 | 82 | if !Verify(public, message, signature) { 83 | t.Errorf("Verify failed on signature from Sign()") 84 | } 85 | } 86 | 87 | func BenchmarkKeyGeneration(b *testing.B) { 88 | var zero zeroReader 89 | for i := 0; i < b.N; i++ { 90 | if _, _, err := GenerateKey(zero); err != nil { 91 | b.Fatal(err) 92 | } 93 | } 94 | } 95 | 96 | func BenchmarkSigning(b *testing.B) { 97 | var zero zeroReader 98 | _, priv, err := GenerateKey(zero) 99 | if err != nil { 100 | b.Fatal(err) 101 | } 102 | message := []byte("Hello, world!") 103 | b.ResetTimer() 104 | for i := 0; i < b.N; i++ { 105 | Sign(priv, message) 106 | } 107 | } 108 | 109 | func BenchmarkVerification(b *testing.B) { 110 | var zero zeroReader 111 | pub, priv, err := GenerateKey(zero) 112 | if err != nil { 113 | b.Fatal(err) 114 | } 115 | message := []byte("Hello, world!") 116 | signature := Sign(priv, message) 117 | b.ResetTimer() 118 | for i := 0; i < b.N; i++ { 119 | Verify(pub, message, signature) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /apps/server/src/email/templates/base.html: -------------------------------------------------------------------------------- 1 | {{define "base"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Email Confirmation 9 | 10 | 132 | 133 | 134 | 135 | {{ template "body" .}} 136 | 137 | 138 | {{end}} -------------------------------------------------------------------------------- /kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | apiVersion: apps/v1 3 | metadata: 4 | name: boompow-deployment 5 | namespace: boompow-next 6 | labels: 7 | app: boompow 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: boompow 13 | template: 14 | metadata: 15 | labels: 16 | app: boompow 17 | spec: 18 | containers: 19 | - name: boompow 20 | image: replaceme 21 | resources: 22 | requests: 23 | cpu: 100m 24 | memory: 200Mi 25 | limits: 26 | cpu: 500m 27 | memory: 1Gi 28 | command: ['/bin/sh', '-c'] 29 | args: ['boompow-server -runServer'] 30 | ports: 31 | - containerPort: 8080 32 | imagePullPolicy: 'Always' 33 | env: 34 | - name: SMTP_PORT 35 | value: '587' 36 | - name: SMTP_SERVER 37 | valueFrom: 38 | secretKeyRef: 39 | name: boompow 40 | key: smtp_server 41 | - name: SMTP_USERNAME 42 | valueFrom: 43 | secretKeyRef: 44 | name: boompow 45 | key: smtp_username 46 | - name: SMTP_PASSWORD 47 | valueFrom: 48 | secretKeyRef: 49 | name: boompow 50 | key: smtp_password 51 | - name: DB_HOST 52 | value: pg-boompow.boompow-next 53 | - name: DB_PORT 54 | value: '5432' 55 | - name: DB_SSLMODE 56 | value: disable 57 | - name: DB_NAME 58 | value: postgres 59 | - name: DB_USER 60 | value: postgres 61 | - name: DB_PASS 62 | valueFrom: 63 | secretKeyRef: 64 | name: boompow 65 | key: db_password 66 | - name: REDIS_HOST 67 | value: redis.redis 68 | - name: REDIS_DB 69 | value: '18' 70 | - name: PRIV_KEY 71 | valueFrom: 72 | secretKeyRef: 73 | name: boompow 74 | key: jwt_signing_key 75 | - name: BPOW_WALLET_ID 76 | valueFrom: 77 | secretKeyRef: 78 | name: boompow 79 | key: wallet_id 80 | - name: BPOW_PRIZE_POOL 81 | valueFrom: 82 | secretKeyRef: 83 | name: boompow 84 | key: prize_pool 85 | - name: BPOW_BANNED_REWARDS 86 | valueFrom: 87 | secretKeyRef: 88 | name: boompow 89 | key: banned_rewards 90 | - name: BPOW_ALLOWED_EMAILS 91 | valueFrom: 92 | secretKeyRef: 93 | name: boompow 94 | key: allowed_emails 95 | - name: BPOW_SERVICE_TOKENS 96 | valueFrom: 97 | secretKeyRef: 98 | name: boompow 99 | key: service_tokens 100 | - name: BPOW_WALLET_ADDRESS 101 | value: ban_1boompow14irck1yauquqypt7afqrh8b6bbu5r93pc6hgbqs7z6o99frcuym 102 | - name: ENVIRONMENT 103 | value: production 104 | - name: BANANO_WS_URL 105 | value: ws://10.4.0.1:7074 106 | - name: NANO_WS_URL 107 | value: ws://10.7.0.1:7078 108 | -------------------------------------------------------------------------------- /apps/server/src/tests/user_repo_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/bananocoin/boompow/apps/server/graph/model" 9 | "github.com/bananocoin/boompow/apps/server/src/database" 10 | "github.com/bananocoin/boompow/apps/server/src/models" 11 | "github.com/bananocoin/boompow/apps/server/src/repository" 12 | utils "github.com/bananocoin/boompow/libs/utils/testing" 13 | ) 14 | 15 | // Test user repo 16 | func TestUserRepo(t *testing.T) { 17 | os.Setenv("MOCK_REDIS", "true") 18 | mockDb, err := database.NewConnection(&database.Config{ 19 | Host: os.Getenv("DB_MOCK_HOST"), 20 | Port: os.Getenv("DB_MOCK_PORT"), 21 | Password: os.Getenv("DB_MOCK_PASS"), 22 | User: os.Getenv("DB_MOCK_USER"), 23 | SSLMode: os.Getenv("DB_SSLMODE"), 24 | DBName: "testing", 25 | }) 26 | utils.AssertEqual(t, nil, err) 27 | err = database.DropAndCreateTables(mockDb) 28 | utils.AssertEqual(t, nil, err) 29 | userRepo := repository.NewUserService(mockDb) 30 | 31 | // Create user 32 | banAddress := "ban_3bsnis6ha3m9cepuaywskn9jykdggxcu8mxsp76yc3oinrt3n7gi77xiggtm" 33 | user, err := userRepo.CreateUser(&model.UserInput{ 34 | Email: "joe@gmail.com", 35 | Password: "Password123!", 36 | Type: model.UserType(models.PROVIDER), 37 | BanAddress: &banAddress, 38 | }, false) 39 | utils.AssertEqual(t, nil, err) 40 | 41 | // Get user 42 | dbUser, err := userRepo.GetUser(&user.ID, nil) 43 | utils.AssertEqual(t, user.ID, dbUser.ID) 44 | utils.AssertEqual(t, "joe@gmail.com", dbUser.Email) 45 | utils.AssertEqual(t, false, dbUser.EmailVerified) 46 | utils.AssertEqual(t, banAddress, *dbUser.BanAddress) 47 | utils.AssertEqual(t, false, dbUser.CanRequestWork) 48 | utils.AssertEqual(t, models.PROVIDER, dbUser.Type) 49 | 50 | // Test confirm emial 51 | token, err := database.GetRedisDB().GetConfirmationToken(dbUser.Email) 52 | utils.AssertEqual(t, nil, err) 53 | 54 | userRepo.VerifyEmailToken(&model.VerifyEmailInput{ 55 | Email: dbUser.Email, 56 | Token: token, 57 | }) 58 | 59 | dbUser, err = userRepo.GetUser(&user.ID, nil) 60 | utils.AssertEqual(t, true, dbUser.EmailVerified) 61 | 62 | // Test authenticate 63 | authenticated := userRepo.Authenticate(&model.LoginInput{ 64 | Email: "joe@gmail.com", 65 | Password: "Password123!", 66 | }) 67 | utils.AssertEqual(t, true, authenticated != nil) 68 | authenticated = userRepo.Authenticate(&model.LoginInput{ 69 | Email: "joe@gmail.com", 70 | Password: "wrongPassword", 71 | }) 72 | utils.AssertEqual(t, true, authenticated == nil) 73 | 74 | // Test # Services 75 | services, err := userRepo.GetNumberServices() 76 | utils.AssertEqual(t, nil, err) 77 | utils.AssertEqual(t, 0, int(services)) 78 | // Create a service 79 | sname := "Service1" 80 | sWeb := "https://google.com" 81 | _, err = userRepo.CreateUser(&model.UserInput{ 82 | Email: "jeff@gmail.com", 83 | Password: "Password123!", 84 | Type: model.UserType(models.REQUESTER), 85 | ServiceName: &sname, 86 | ServiceWebsite: &sWeb, 87 | }, false) 88 | utils.AssertEqual(t, nil, err) 89 | services, err = userRepo.GetNumberServices() 90 | utils.AssertEqual(t, nil, err) 91 | utils.AssertEqual(t, 1, int(services)) 92 | 93 | // Test delete user 94 | userRepo.DeleteUser(user.ID) 95 | dbUser, err = userRepo.GetUser(&user.ID, nil) 96 | utils.AssertEqual(t, true, err != nil) 97 | 98 | // TEst generate service token 99 | token = userRepo.GenerateServiceToken() 100 | utils.AssertEqual(t, 44, len(token)) 101 | utils.AssertEqual(t, true, strings.HasPrefix(token, "service:")) 102 | 103 | } 104 | -------------------------------------------------------------------------------- /libs/utils/net/ip.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | ) 7 | 8 | // Some data center ranges we want to block 9 | var hetznerRanges = []string{ 10 | "116.202.0.0/16", 11 | "116.203.0.0/16", 12 | "128.140.0.0/17", 13 | "135.181.0.0/16", 14 | "136.243.0.0/16", 15 | "138.201.0.0/16", 16 | "142.132.128.0/17", 17 | "144.76.0.0/16", 18 | "148.251.0.0/16", 19 | "157.90.0.0/16", 20 | "159.69.0.0/16", 21 | "162.55.0.0/16", 22 | "167.233.0.0/16", 23 | "167.235.0.0/16", 24 | "168.119.0.0/16", 25 | "171.25.225.0/24", 26 | "176.9.0.0/16", 27 | "178.212.75.0/24", 28 | "178.63.0.0/16", 29 | "185.107.52.0/22", 30 | "185.110.95.0/24", 31 | "185.112.180.0/24", 32 | "185.126.28.0/22", 33 | "185.12.65.0/24", 34 | "185.136.140.0/23", 35 | "185.157.176.0/23", 36 | "185.157.178.0/23", 37 | "185.157.83.0/24", 38 | "185.171.224.0/22", 39 | "185.189.228.0/24", 40 | "185.189.229.0/24", 41 | "185.189.230.0/24", 42 | "185.189.231.0/24", 43 | "185.209.124.0/22", 44 | "185.213.45.0/24", 45 | "185.216.237.0/24", 46 | "185.226.99.0/24", 47 | "185.228.8.0/23", 48 | "185.242.76.0/24", 49 | "185.36.144.0/22", 50 | "185.50.120.0/23", 51 | "188.34.128.0/17", 52 | "188.40.0.0/16", 53 | "193.110.6.0/23", 54 | "193.163.198.0/24", 55 | "193.25.170.0/23", 56 | "194.35.12.0/23", 57 | "194.42.180.0/22", 58 | "194.42.184.0/22", 59 | "194.62.106.0/24", 60 | "195.201.0.0/16", 61 | "195.248.224.0/24", 62 | "195.60.226.0/24", 63 | "195.96.156.0/24", 64 | "197.242.84.0/22", 65 | "201.131.3.0/24", 66 | "213.133.96.0/19", 67 | "213.232.193.0/24", 68 | "213.239.192.0/18", 69 | "23.88.0.0/17", 70 | "45.148.28.0/22", 71 | "45.15.120.0/22", 72 | "46.4.0.0/16", 73 | "49.12.0.0/16", 74 | "49.13.0.0/16", 75 | "5.75.128.0/17", 76 | "5.9.0.0/16", 77 | "78.46.0.0/15", 78 | "83.219.100.0/22", 79 | "83.243.120.0/22", 80 | "85.10.192.0/18", 81 | "88.198.0.0/16", 82 | "88.99.0.0/16", 83 | "91.107.128.0/17", 84 | "91.190.240.0/21", 85 | "91.233.8.0/22", 86 | "94.130.0.0/16", 87 | "94.154.121.0/24", 88 | "95.217.0.0/16", 89 | "95.216.0.0/16", 90 | "65.21.0.0/16", 91 | "65.109.0.0/16", 92 | "65.108.0.0/16", 93 | "45.136.70.0/23", 94 | "135.181.0.0/16", 95 | "2a01:4f8::/32", 96 | "2a01:4f9::/32", 97 | "2a01:4ff:ff01::/48", 98 | "2a01:b140::/29", 99 | "2a06:1301:4050::/48", 100 | "2a06:be80::/29", 101 | "2a0e:2c80::/29", 102 | "2a11:48c0::/29", 103 | "2a11:e980::/29", 104 | "2a12:e00::/29", 105 | } 106 | 107 | type IPMatcher struct { 108 | IP net.IP 109 | SubNet *net.IPNet 110 | } 111 | type IPMatchers []*IPMatcher 112 | 113 | func NewIPMatcher(ipStr string) (*IPMatcher, error) { 114 | ip, subNet, err := net.ParseCIDR(ipStr) 115 | if err != nil { 116 | ip = net.ParseIP(ipStr) 117 | if ip == nil { 118 | return nil, errors.New("invalid IP: " + ipStr) 119 | } 120 | } 121 | return &IPMatcher{ip, subNet}, nil 122 | } 123 | 124 | func (m IPMatcher) Match(ipStr string) bool { 125 | ip := net.ParseIP(ipStr) 126 | if ip == nil { 127 | return false 128 | } 129 | return m.IP.Equal(ip) || m.SubNet != nil && m.SubNet.Contains(ip) 130 | } 131 | 132 | func NewIPMatchers(ips []string) (list IPMatchers, err error) { 133 | for _, ipStr := range ips { 134 | var m *IPMatcher 135 | m, err = NewIPMatcher(ipStr) 136 | if err != nil { 137 | return 138 | } 139 | list = append(list, m) 140 | } 141 | return 142 | } 143 | 144 | func IPContains(ipMatchers []*IPMatcher, ip string) bool { 145 | for _, m := range ipMatchers { 146 | if m.Match(ip) { 147 | return true 148 | } 149 | } 150 | return false 151 | } 152 | 153 | func IsIPInHetznerRange(ip string) bool { 154 | for _, rangeStr := range hetznerRanges { 155 | matcher, err := NewIPMatcher(rangeStr) 156 | if err != nil { 157 | return true 158 | } 159 | if matcher.Match(ip) { 160 | return true 161 | } 162 | } 163 | return false 164 | } 165 | -------------------------------------------------------------------------------- /apps/client/websocket/client.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/bananocoin/boompow/apps/client/models" 10 | serializableModels "github.com/bananocoin/boompow/libs/models" 11 | ) 12 | 13 | type WebsocketService struct { 14 | WS *RecConn 15 | AuthToken string 16 | URL string 17 | maxDifficulty int 18 | minDifficulty int 19 | skipPrecache bool 20 | } 21 | 22 | func NewWebsocketService(url string, maxDifficulty int, minDifficulty int, skipPrecache bool) *WebsocketService { 23 | return &WebsocketService{ 24 | WS: &RecConn{}, 25 | URL: url, 26 | maxDifficulty: maxDifficulty, 27 | minDifficulty: minDifficulty, 28 | skipPrecache: skipPrecache, 29 | } 30 | } 31 | 32 | func (ws *WebsocketService) SetAuthToken(authToken string) { 33 | ws.AuthToken = authToken 34 | ws.WS.setReqHeader(http.Header{ 35 | "Authorization": {ws.AuthToken}, 36 | }) 37 | } 38 | 39 | func (ws *WebsocketService) StartWSClient(ctx context.Context, workQueueChan chan *serializableModels.ClientMessage, queue *models.RandomAccessQueue) { 40 | if ws.AuthToken == "" { 41 | panic("Tired to start websocket client without auth token") 42 | } 43 | // Start the websocket connection 44 | ws.WS.Dial(ws.URL, http.Header{ 45 | "Authorization": {ws.AuthToken}, 46 | }) 47 | 48 | for { 49 | select { 50 | case <-ctx.Done(): 51 | go ws.WS.Close() 52 | fmt.Printf("Websocket closed %s", ws.WS.GetURL()) 53 | return 54 | default: 55 | if !ws.WS.IsConnected() { 56 | fmt.Printf("Websocket disconnected %s", ws.WS.GetURL()) 57 | time.Sleep(2 * time.Second) 58 | continue 59 | } 60 | 61 | var serverMsg serializableModels.ClientMessage 62 | err := ws.WS.ReadJSON(&serverMsg) 63 | if err != nil { 64 | fmt.Printf("Error: ReadJSON %s", ws.WS.GetURL()) 65 | continue 66 | } 67 | 68 | // Determine type of message 69 | if serverMsg.MessageType == serializableModels.WorkGenerate { 70 | if serverMsg.DifficultyMultiplier > ws.maxDifficulty { 71 | fmt.Printf("\n😒 Ignoring work request %s with difficulty %dx above our max %dx", serverMsg.Hash, serverMsg.DifficultyMultiplier, ws.maxDifficulty) 72 | continue 73 | } 74 | if serverMsg.DifficultyMultiplier < ws.minDifficulty { 75 | fmt.Printf("\n😒 Ignoring work request %s with difficulty %dx below our min %dx", serverMsg.Hash, serverMsg.DifficultyMultiplier, ws.minDifficulty) 76 | continue 77 | } 78 | 79 | if ws.skipPrecache && serverMsg.Precache { 80 | fmt.Printf("\n😒 Ignoring precache request %s", serverMsg.Hash) 81 | continue 82 | } 83 | 84 | fmt.Printf("\n🦋 Received work request %s with difficulty %dx", serverMsg.Hash, serverMsg.DifficultyMultiplier) 85 | 86 | if len(serverMsg.Hash) != 64 { 87 | fmt.Printf("\nReceived invalid hash, skipping") 88 | continue 89 | } 90 | 91 | // If the backlog is too large, no-op 92 | if queue.Len() > 99 { 93 | fmt.Printf("\nBacklog is too large, skipping hash %s", serverMsg.Hash) 94 | continue 95 | } 96 | 97 | // Queue this work 98 | queue.Put(serverMsg) 99 | 100 | // Signal channel that we have work to do 101 | workQueueChan <- &serverMsg 102 | } else if serverMsg.MessageType == serializableModels.WorkCancel { 103 | // Delete pending work from queue 104 | // ! TODO - can we cancel currently runing work calculations? 105 | queue.Delete(serverMsg.Hash) 106 | } else if serverMsg.MessageType == serializableModels.BlockAwarded { 107 | fmt.Printf("\n💰 Received block awarded %s", serverMsg.Hash) 108 | fmt.Printf("\n💰 Your current estimated next payout is %f%% or %f BAN", serverMsg.PercentOfPool, serverMsg.EstimatedAward) 109 | } else { 110 | fmt.Printf("\n🦋 Received unknown message %s\n", serverMsg.MessageType) 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /apps/server/src/net/nanows_client.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | guuid "github.com/google/uuid" 12 | "github.com/recws-org/recws" 13 | "k8s.io/klog/v2" 14 | ) 15 | 16 | type wsSubscribe struct { 17 | Action string `json:"action"` 18 | Topic string `json:"topic"` 19 | Ack bool `json:"ack"` 20 | Id string `json:"id"` 21 | Options map[string][]string `json:"options"` 22 | } 23 | 24 | type ConfirmationResponse struct { 25 | Topic string `json:"topic"` 26 | Time string `json:"time"` 27 | Message map[string]interface{} `json:"message"` 28 | } 29 | 30 | type WSCallbackBlock struct { 31 | Type string `json:"type"` 32 | Account string `json:"account"` 33 | Previous string `json:"previous"` 34 | Representative string `json:"representative"` 35 | Balance string `json:"balance"` 36 | Link string `json:"link"` 37 | LinkAsAccount string `json:"link_as_account"` 38 | Work string `json:"work"` 39 | Signature string `json:"signature"` 40 | Destination string `json:"destination"` 41 | Source string `json:"source"` 42 | Subtype string `json:"subtype"` 43 | } 44 | 45 | type WSCallbackMsg struct { 46 | IsSend string `json:"is_send"` 47 | Block WSCallbackBlock `json:"block"` 48 | Account string `json:"account"` 49 | Hash string `json:"hash"` 50 | Amount string `json:"amount"` 51 | } 52 | 53 | func StartNanoWSClient(wsUrl string, callbackChan *chan *WSCallbackMsg) { 54 | ctx, cancel := context.WithCancel(context.Background()) 55 | sentSubscribe := false 56 | ws := recws.RecConn{} 57 | // Nano subscription request 58 | subRequest := wsSubscribe{ 59 | Action: "subscribe", 60 | Topic: "confirmation", 61 | Ack: false, 62 | Id: guuid.New().String(), 63 | // ! TODO - subscribe to only connected acounts 64 | // Options: map[string][]string{ 65 | // "accounts": { 66 | // account, 67 | // }, 68 | // }, 69 | } 70 | ws.Dial(wsUrl, nil) 71 | 72 | sigc := make(chan os.Signal, 1) 73 | signal.Notify(sigc, 74 | syscall.SIGHUP, 75 | syscall.SIGINT, 76 | syscall.SIGTERM, 77 | syscall.SIGQUIT) 78 | defer func() { 79 | signal.Stop(sigc) 80 | cancel() 81 | }() 82 | 83 | for { 84 | select { 85 | case <-sigc: 86 | cancel() 87 | return 88 | case <-ctx.Done(): 89 | go ws.Close() 90 | klog.Infof("Websocket closed %s", ws.GetURL()) 91 | return 92 | default: 93 | if !ws.IsConnected() { 94 | sentSubscribe = false 95 | klog.Infof("Websocket disconnected %s", ws.GetURL()) 96 | time.Sleep(2 * time.Second) 97 | continue 98 | } 99 | 100 | // Sent subscribe with ack 101 | if !sentSubscribe { 102 | if err := ws.WriteJSON(subRequest); err != nil { 103 | klog.Infof("Error sending subscribe request %s", ws.GetURL()) 104 | time.Sleep(2 * time.Second) 105 | continue 106 | } else { 107 | sentSubscribe = true 108 | } 109 | } 110 | 111 | var confMessage ConfirmationResponse 112 | err := ws.ReadJSON(&confMessage) 113 | if err != nil { 114 | klog.Infof("Error: ReadJSON %s", ws.GetURL()) 115 | sentSubscribe = false 116 | continue 117 | } 118 | 119 | // Trigger callback 120 | if confMessage.Topic == "confirmation" { 121 | var deserialized WSCallbackMsg 122 | serialized, err := json.Marshal(confMessage.Message) 123 | if err != nil { 124 | klog.Infof("Error: Marshal ws %v", err) 125 | continue 126 | } 127 | if err := json.Unmarshal(serialized, &deserialized); err != nil { 128 | klog.Errorf("Error: decoding the callback to WSCallbackMsg %v", err) 129 | continue 130 | } 131 | *callbackChan <- &deserialized 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /apps/server/src/controller/worker_ws.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/bananocoin/boompow/apps/server/src/middleware" 9 | "github.com/bananocoin/boompow/libs/utils/net" 10 | "github.com/gorilla/websocket" 11 | "k8s.io/klog/v2" 12 | ) 13 | 14 | var ( 15 | newline = []byte{'\n'} 16 | space = []byte{' '} 17 | ) 18 | 19 | type ClientWSMessage struct { 20 | ClientEmail string `json:"email"` 21 | msg []byte 22 | } 23 | 24 | // readPump pumps messages from the websocket connection to the hub. 25 | // 26 | // The application runs readPump in a per-connection goroutine. The application 27 | // ensures that there is at most one reader on a connection by executing all 28 | // reads from this goroutine. 29 | func (c *Client) readPump() { 30 | defer func() { 31 | c.Hub.Unregister <- c 32 | c.Conn.Close() 33 | }() 34 | c.Conn.SetReadLimit(MaxMessageSize) 35 | c.Conn.SetReadDeadline(time.Now().Add(PongWait)) 36 | c.Conn.SetPongHandler(func(string) error { c.Conn.SetReadDeadline(time.Now().Add(PongWait)); return nil }) 37 | for { 38 | _, message, err := c.Conn.ReadMessage() 39 | if err != nil { 40 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 41 | klog.Errorf("error: %v", err) 42 | } 43 | break 44 | } 45 | message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) 46 | msgObj := ClientWSMessage{ClientEmail: c.Email, msg: message} 47 | c.Hub.Response <- msgObj 48 | } 49 | } 50 | 51 | // writePump pumps messages from the hub to the websocket connection. 52 | // 53 | // A goroutine running writePump is started for each connection. The 54 | // application ensures that there is at most one writer to a connection by 55 | // executing all writes from this goroutine. 56 | func (c *Client) writePump() { 57 | ticker := time.NewTicker(PingPeriod) 58 | defer func() { 59 | ticker.Stop() 60 | c.Conn.Close() 61 | }() 62 | for { 63 | select { 64 | case message, ok := <-c.Send: 65 | c.Conn.SetWriteDeadline(time.Now().Add(WriteWait)) 66 | if !ok { 67 | // The hub closed the channel. 68 | c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) 69 | return 70 | } 71 | 72 | w, err := c.Conn.NextWriter(websocket.TextMessage) 73 | if err != nil { 74 | return 75 | } 76 | w.Write(message) 77 | 78 | if err := w.Close(); err != nil { 79 | return 80 | } 81 | case <-ticker.C: 82 | c.Conn.SetWriteDeadline(time.Now().Add(WriteWait)) 83 | if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { 84 | return 85 | } 86 | } 87 | } 88 | } 89 | 90 | // serveWs handles websocket requests from the peer. 91 | func WorkerChl(hub *Hub, w http.ResponseWriter, r *http.Request) { 92 | provider := middleware.AuthorizedProvider(r.Context()) 93 | // Only PROVIDER type users can provide work 94 | if provider == nil { 95 | w.WriteHeader(http.StatusUnauthorized) 96 | w.Write([]byte("401 - Unauthorized")) 97 | return 98 | } 99 | 100 | clientIP := net.GetIPAddress(r) 101 | 102 | // Block hetzner datacenters 103 | if net.IsIPInHetznerRange(clientIP) { 104 | w.WriteHeader(http.StatusForbidden) 105 | w.Write([]byte("403 - Forbidden")) 106 | return 107 | } 108 | 109 | // Block IPs already connected 110 | if hub.AlreadyConnected(clientIP) { 111 | w.WriteHeader(http.StatusForbidden) 112 | w.Write([]byte("403 - Forbidden")) 113 | return 114 | } 115 | 116 | conn, err := Upgrader.Upgrade(w, r, nil) 117 | if err != nil { 118 | klog.Error(err) 119 | return 120 | } 121 | client := &Client{Hub: hub, Conn: conn, Send: make(chan []byte, 256), IPAddress: clientIP, Email: provider.User.Email} 122 | client.Hub.Register <- client 123 | 124 | // Allow collection of memory referenced by the caller by doing all work in 125 | // new goroutines. 126 | go client.writePump() 127 | go client.readPump() 128 | } 129 | -------------------------------------------------------------------------------- /apps/client/gql/generated.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/Khan/genqlient, DO NOT EDIT. 2 | 3 | package gql 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/Khan/genqlient/graphql" 9 | ) 10 | 11 | type LoginInput struct { 12 | Email string `json:"email"` 13 | Password string `json:"password"` 14 | } 15 | 16 | // GetEmail returns LoginInput.Email, and is useful for accessing the field via an interface. 17 | func (v *LoginInput) GetEmail() string { return v.Email } 18 | 19 | // GetPassword returns LoginInput.Password, and is useful for accessing the field via an interface. 20 | func (v *LoginInput) GetPassword() string { return v.Password } 21 | 22 | type RefreshTokenInput struct { 23 | Token string `json:"token"` 24 | } 25 | 26 | // GetToken returns RefreshTokenInput.Token, and is useful for accessing the field via an interface. 27 | func (v *RefreshTokenInput) GetToken() string { return v.Token } 28 | 29 | // __loginUserInput is used internally by genqlient 30 | type __loginUserInput struct { 31 | Input LoginInput `json:"input"` 32 | } 33 | 34 | // GetInput returns __loginUserInput.Input, and is useful for accessing the field via an interface. 35 | func (v *__loginUserInput) GetInput() LoginInput { return v.Input } 36 | 37 | // __refreshTokenInput is used internally by genqlient 38 | type __refreshTokenInput struct { 39 | Input RefreshTokenInput `json:"input"` 40 | } 41 | 42 | // GetInput returns __refreshTokenInput.Input, and is useful for accessing the field via an interface. 43 | func (v *__refreshTokenInput) GetInput() RefreshTokenInput { return v.Input } 44 | 45 | // loginUserLoginLoginResponse includes the requested fields of the GraphQL type LoginResponse. 46 | type loginUserLoginLoginResponse struct { 47 | Token string `json:"token"` 48 | } 49 | 50 | // GetToken returns loginUserLoginLoginResponse.Token, and is useful for accessing the field via an interface. 51 | func (v *loginUserLoginLoginResponse) GetToken() string { return v.Token } 52 | 53 | // loginUserResponse is returned by loginUser on success. 54 | type loginUserResponse struct { 55 | Login loginUserLoginLoginResponse `json:"login"` 56 | } 57 | 58 | // GetLogin returns loginUserResponse.Login, and is useful for accessing the field via an interface. 59 | func (v *loginUserResponse) GetLogin() loginUserLoginLoginResponse { return v.Login } 60 | 61 | // refreshTokenResponse is returned by refreshToken on success. 62 | type refreshTokenResponse struct { 63 | RefreshToken string `json:"refreshToken"` 64 | } 65 | 66 | // GetRefreshToken returns refreshTokenResponse.RefreshToken, and is useful for accessing the field via an interface. 67 | func (v *refreshTokenResponse) GetRefreshToken() string { return v.RefreshToken } 68 | 69 | func loginUser( 70 | ctx context.Context, 71 | client graphql.Client, 72 | input LoginInput, 73 | ) (*loginUserResponse, error) { 74 | req := &graphql.Request{ 75 | OpName: "loginUser", 76 | Query: ` 77 | mutation loginUser ($input: LoginInput!) { 78 | login(input: $input) { 79 | token 80 | } 81 | } 82 | `, 83 | Variables: &__loginUserInput{ 84 | Input: input, 85 | }, 86 | } 87 | var err error 88 | 89 | var data loginUserResponse 90 | resp := &graphql.Response{Data: &data} 91 | 92 | err = client.MakeRequest( 93 | ctx, 94 | req, 95 | resp, 96 | ) 97 | 98 | return &data, err 99 | } 100 | 101 | func refreshToken( 102 | ctx context.Context, 103 | client graphql.Client, 104 | input RefreshTokenInput, 105 | ) (*refreshTokenResponse, error) { 106 | req := &graphql.Request{ 107 | OpName: "refreshToken", 108 | Query: ` 109 | mutation refreshToken ($input: RefreshTokenInput!) { 110 | refreshToken(input: $input) 111 | } 112 | `, 113 | Variables: &__refreshTokenInput{ 114 | Input: input, 115 | }, 116 | } 117 | var err error 118 | 119 | var data refreshTokenResponse 120 | resp := &graphql.Response{Data: &data} 121 | 122 | err = client.MakeRequest( 123 | ctx, 124 | req, 125 | resp, 126 | ) 127 | 128 | return &data, err 129 | } 130 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /logo_green.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /apps/server/graph/model/models_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package model 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "strconv" 9 | ) 10 | 11 | type ChangePasswordInput struct { 12 | NewPassword string `json:"newPassword"` 13 | } 14 | 15 | type GetUserResponse struct { 16 | Email string `json:"email"` 17 | Type UserType `json:"type"` 18 | BanAddress *string `json:"banAddress"` 19 | ServiceName *string `json:"serviceName"` 20 | ServiceWebsite *string `json:"serviceWebsite"` 21 | EmailVerified bool `json:"emailVerified"` 22 | CanRequestWork bool `json:"canRequestWork"` 23 | } 24 | 25 | type LoginInput struct { 26 | Email string `json:"email"` 27 | Password string `json:"password"` 28 | } 29 | 30 | type LoginResponse struct { 31 | Token string `json:"token"` 32 | Email string `json:"email"` 33 | Type UserType `json:"type"` 34 | BanAddress *string `json:"banAddress"` 35 | ServiceName *string `json:"serviceName"` 36 | ServiceWebsite *string `json:"serviceWebsite"` 37 | EmailVerified bool `json:"emailVerified"` 38 | } 39 | 40 | type RefreshTokenInput struct { 41 | Token string `json:"token"` 42 | } 43 | 44 | type ResendConfirmationEmailInput struct { 45 | Email string `json:"email"` 46 | } 47 | 48 | type ResetPasswordInput struct { 49 | Email string `json:"email"` 50 | } 51 | 52 | type Stats struct { 53 | ConnectedWorkers int `json:"connectedWorkers"` 54 | TotalPaidBanano string `json:"totalPaidBanano"` 55 | RegisteredServiceCount int `json:"registeredServiceCount"` 56 | Top10 []*StatsUserType `json:"top10"` 57 | Services []*StatsServiceType `json:"services"` 58 | } 59 | 60 | type StatsServiceType struct { 61 | Name string `json:"name"` 62 | Website string `json:"website"` 63 | Requests int `json:"requests"` 64 | } 65 | 66 | type StatsUserType struct { 67 | BanAddress string `json:"banAddress"` 68 | TotalPaidBanano string `json:"totalPaidBanano"` 69 | } 70 | 71 | type User struct { 72 | ID string `json:"id"` 73 | Email string `json:"email"` 74 | CreatedAt string `json:"createdAt"` 75 | UpdatedAt string `json:"updatedAt"` 76 | Type UserType `json:"type"` 77 | BanAddress *string `json:"banAddress"` 78 | } 79 | 80 | type UserInput struct { 81 | Email string `json:"email"` 82 | Password string `json:"password"` 83 | Type UserType `json:"type"` 84 | BanAddress *string `json:"banAddress"` 85 | ServiceName *string `json:"serviceName"` 86 | ServiceWebsite *string `json:"serviceWebsite"` 87 | } 88 | 89 | type VerifyEmailInput struct { 90 | Email string `json:"email"` 91 | Token string `json:"token"` 92 | } 93 | 94 | type VerifyServiceInput struct { 95 | Email string `json:"email"` 96 | Token string `json:"token"` 97 | } 98 | 99 | type WorkGenerateInput struct { 100 | Hash string `json:"hash"` 101 | DifficultyMultiplier int `json:"difficultyMultiplier"` 102 | BlockAward *bool `json:"blockAward"` 103 | } 104 | 105 | type UserType string 106 | 107 | const ( 108 | UserTypeProvider UserType = "PROVIDER" 109 | UserTypeRequester UserType = "REQUESTER" 110 | ) 111 | 112 | var AllUserType = []UserType{ 113 | UserTypeProvider, 114 | UserTypeRequester, 115 | } 116 | 117 | func (e UserType) IsValid() bool { 118 | switch e { 119 | case UserTypeProvider, UserTypeRequester: 120 | return true 121 | } 122 | return false 123 | } 124 | 125 | func (e UserType) String() string { 126 | return string(e) 127 | } 128 | 129 | func (e *UserType) UnmarshalGQL(v interface{}) error { 130 | str, ok := v.(string) 131 | if !ok { 132 | return fmt.Errorf("enums must be strings") 133 | } 134 | 135 | *e = UserType(str) 136 | if !e.IsValid() { 137 | return fmt.Errorf("%s is not a valid UserType", str) 138 | } 139 | return nil 140 | } 141 | 142 | func (e UserType) MarshalGQL(w io.Writer) { 143 | fmt.Fprint(w, strconv.Quote(e.String())) 144 | } 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Release](https://img.shields.io/github/v/release/BananoCoin/boompow)](https://github.com/BananoCoin/boompow/releases/latest) ![GitHub go.mod Go version (subdirectory of monorepo)](https://img.shields.io/github/go-mod/go-version/bananocoin/boompow?filename=apps%2Fserver%2Fgo.mod) [![License](https://img.shields.io/github/license/BananoCoin/boompow-next)](https://github.com/bananocoin/boompow/blob/master/LICENSE) [![CI](https://github.com/bananocoin/boompow/workflows/CI/badge.svg)](https://github.com/bananocoin/boompow/actions?query=workflow%3ACI) 2 | 3 |

4 | 5 |

6 | 7 | This is a distributed proof of work system for the [BANANO](https://banano.cc) and [NANO](https://nano.org) cryptocurrencies. 8 | 9 | ## What is It? 10 | 11 | Banano transactions require a "proof of work" in order to be broadcasted and confirmed on the network. Basically you need to compute a series of random hashes until you find one that is "valid" (satisifies the difficulty equation). This serves as a replacement for a transaction fee. 12 | 13 | ## Why do I want BoomPow? 14 | 15 | The proof of work required for a BANANO transasction can be calculated within a couple seconds on most modern computers. Which begs the question "why does it matter?" 16 | 17 | 1. There's applications that require large volumes of PoW, while an individual calculation can be acceptably fast - it is different when it's overloaded with hundreds of problems to solve all at the same time. 18 | - The [Graham TipBot](https://github.com/bbedward/Graham_Nano_Tip_Bot) has been among the biggest block producers on the NANO and BANANO networks for more than a year. Requiring tens of thousands of calculations every month. 19 | - The [Twitter and Telegram TipBots](https://github.com/mitche50/NanoTipBot) also calculate PoW for every transaction 20 | - [Kalium](https://kalium.banano.cc) and [Natrium](https://natrium.io) are the most widely used wallets on the NANO and BANANO networks with more than 10,000 users each. They all demand PoW whenever they make or send a transaction. 21 | - There's many other popular casinos, exchanges, and other applications that can benefit from a highly-available, highly-reliable PoW service. 22 | 2. While a single PoW (for BANANO) can be calculated fairly quickly on modern hardware, there are some scenarios in which sub-second PoW is highly desired. 23 | - [Kalium](https://kalium.banano.cc) and [Natrium](https://natrium.io) are the top wallets for BANANO and NANO. People use these wallets to showcase BANANO or NANO to their friends, to send money when they need to, they're used in promotional videos on YouTube, Twitter, and other platforms. _Fast_ PoW is an absolute must for these services - the BoomPow system will provide incredibly fast proof of work from people who contribute using high-end hardware. 24 | 25 | All of the aforementioned services will use the BoomPow system, and others services are free to request access as well. 26 | 27 | ## Who is Paying for this "High-End" Hardware? 28 | 29 | [BANANO](https://banano.cc) is an instant, feeless, rich in potassium cryptocurrency. It has had an ongoing **free and fair** distribution since April 1st, 2018. 30 | 31 | BANANO is distributed through [folding@home "mining"](https://bananominer.com), faucet games, giveaways, rain parties on telegram and discord, and more. We are always looking for new ways to distribute BANANO _fairly._ 32 | 33 | BoomPow is going to reward contributors with BANANO. Similar to mining, if you provide valid PoW solutions for the BoomPow system you will get regular payments based on how much you contribute. 34 | 35 | ## Components 36 | 37 | This is a GOLang "monorepo" that contains all BoomPoW Services 38 | 39 | - [Server](https://github.com/bananocoin/boompow/blob/master/apps/server) 40 | - [Client](https://github.com/bananocoin/boompow/blob/master/apps/client) 41 | - [Moneybags (Payment Cron)](https://github.com/bananocoin/boompow/blob/master/services/moneybags) 42 | 43 | ## Contributing 44 | 45 | Development for BoomPoW is ideal with a [docker](https://www.docker.com/) development environment. 46 | 47 | These alias may be helpful to add to your `~/.bashrc` or `~/.zshrc` 48 | 49 | ``` 50 | alias dcup="docker-compose up -d" 51 | alias dcdown="docker-compose down" 52 | alias dcbuild="docker-compose build" 53 | alias dczsh="docker-compose exec app /bin/zsh" 54 | alias dcps="docker-compose ps" 55 | alias dcgo="docker-compose exec app go" 56 | alias dcgoclient="docker-compose exec --workdir /app/apps/client app go" 57 | alias dcgoserver="docker-compose exec --workdir /app/apps/server app go" 58 | alias psql-boompow="PGPASSWORD=postgres psql boompow -h localhost -U postgres -p 5433" 59 | ``` 60 | 61 | Once you have docker installed and running, as well as docker-compose, you can start developing with: 62 | 63 | ``` 64 | > dcup 65 | # To run the server 66 | > dcgo run github.com/bananocoin/boomow-next/apps/server -runServer 67 | # To run the client 68 | > dcgo run github.com/bananocoin/boompow/apps/client 69 | ``` 70 | 71 | To get an interactive shell in the container 72 | 73 | ``` 74 | dczsh 75 | ``` 76 | 77 | ## Issues 78 | 79 | For issues, create tickets on the [Issues Page](https://github.com/bananocoin/boompow/issues) 80 | 81 | The [BANANO discord server](https://chat.banano.cc) has a channel dedicated to to boompow if you have general questions about the service. 82 | -------------------------------------------------------------------------------- /services/moneybags/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/bananocoin/boompow/apps/server/src/database" 12 | "github.com/bananocoin/boompow/apps/server/src/repository" 13 | "github.com/bananocoin/boompow/libs/models" 14 | "github.com/bananocoin/boompow/libs/utils" 15 | "github.com/bananocoin/boompow/libs/utils/number" 16 | "github.com/google/uuid" 17 | "github.com/joho/godotenv" 18 | "gorm.io/gorm" 19 | ) 20 | 21 | // The way this process works is: 22 | // 1) We get the unpaid works for each user 23 | // 2) We figure out what percentage of the total prize pool this user has earned 24 | // 3) We build payments for each user based on that amount and save in database 25 | // 4) We ship the payments 26 | 27 | func main() { 28 | dryRun := flag.Bool("dry-run", false, "Dry run") 29 | rpcSend := flag.Bool("rpc-send", false, "Broadcast pending payments") 30 | flag.Parse() 31 | 32 | godotenv.Load() 33 | // Setup database conn 34 | config := &database.Config{ 35 | Host: os.Getenv("DB_HOST"), 36 | Port: os.Getenv("DB_PORT"), 37 | Password: os.Getenv("DB_PASS"), 38 | User: os.Getenv("DB_USER"), 39 | SSLMode: os.Getenv("DB_SSLMODE"), 40 | DBName: os.Getenv("DB_NAME"), 41 | } 42 | fmt.Println("🏡 Connecting to database...") 43 | db, err := database.NewConnection(config) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | userRepo := repository.NewUserService(db) 49 | workRepo := repository.NewWorkService(db, userRepo) 50 | paymentRepo := repository.NewPaymentService(db) 51 | rppClient := NewRPCClient(os.Getenv("RPC_URL")) 52 | 53 | // Do all of this within a transaction 54 | err = db.Transaction(func(tx *gorm.DB) error { 55 | if !*rpcSend { 56 | fmt.Println("👽 Getting unpaid works...") 57 | var res []repository.UnpaidWorkResult 58 | if *dryRun { 59 | fmt.Println("🏃 Dry run mode - not actually sending payments") 60 | res, err = workRepo.GetUnpaidWorkCount(tx) 61 | } else { 62 | res, err = workRepo.GetUnpaidWorkCountAndMarkAllPaid(tx) 63 | } 64 | 65 | if err != nil { 66 | fmt.Printf("❌ Error retrieving unpaid works %v", err) 67 | return err 68 | } 69 | 70 | if len(res) == 0 { 71 | fmt.Println("🤷 No unpaid works found") 72 | return nil 73 | } 74 | 75 | // Compute the entire sum of the unpaid works 76 | totalSum := 0 77 | for _, v := range res { 78 | totalSum += v.DifficultySum 79 | } 80 | 81 | sendRequestsRaw := []models.SendRequest{} 82 | 83 | // Compute the percentage each user has earned and build payments 84 | for _, v := range res { 85 | percentageOfPool := float64(v.DifficultySum) / float64(totalSum) 86 | paymentAmount := percentageOfPool * float64(utils.GetTotalPrizePool()) 87 | 88 | sendRequestsRaw = append(sendRequestsRaw, models.SendRequest{ 89 | BaseRequest: models.SendAction, 90 | Wallet: utils.GetWalletID(), 91 | Source: utils.GetWalletAddress(), 92 | Destination: v.BanAddress, 93 | AmountRaw: number.BananoToRaw(paymentAmount), 94 | // Just a unique payment identifier 95 | ID: fmt.Sprintf("%s:%s", v.BanAddress, uuid.New().String()), 96 | PaidTo: v.ProvidedBy, 97 | }) 98 | 99 | fmt.Printf("💸 %s has earned %f%% of the pool, and will be paid %f\n", v.BanAddress, percentageOfPool*100, paymentAmount) 100 | } 101 | 102 | if !*dryRun { 103 | err = paymentRepo.BatchCreateSendRequests(tx, sendRequestsRaw) 104 | if err != nil { 105 | fmt.Printf("❌ Error creating send requests %v", err) 106 | return err 107 | } 108 | } 109 | return nil 110 | } 111 | 112 | // Alternative job retrieves all payments from database with null block-hash and broadcasts them to the node 113 | fmt.Println("👽 Getting pending payments...") 114 | 115 | payments, err := paymentRepo.GetPendingPayments(tx) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | for _, payment := range payments { 121 | if !*dryRun { 122 | // Keep original to update the database 123 | origPaymentID := strings.Clone(payment.ID) 124 | // Ensure ID is not longer than 64 chars 125 | payment.ID = Sha256(payment.ID) 126 | res, err := rppClient.MakeSendRequest(payment) 127 | if err != nil { 128 | fmt.Printf("\n❌ Error sending payment, ID %s, %v", origPaymentID, err) 129 | fmt.Printf("\nContinuing tho...") 130 | continue 131 | } 132 | fmt.Printf("\n💸 Sent payment, ID %s, %v", origPaymentID, res.Block) 133 | err = paymentRepo.SetBlockHash(tx, origPaymentID, res.Block) 134 | if err != nil { 135 | fmt.Printf("\n❌ Error setting payment block hash, ID %s, hash %s, %v", origPaymentID, res.Block, err) 136 | fmt.Printf("\nContinuing tho...") 137 | } 138 | } else { 139 | fmt.Printf("\n💸 Would send payment, amount %s, to %s", payment.AmountRaw, payment.Destination) 140 | } 141 | } 142 | 143 | return nil 144 | }) 145 | 146 | database.GetRedisDB().WipeClientScores() 147 | 148 | if err != nil { 149 | os.Exit(1) 150 | } 151 | 152 | // Success 153 | os.Exit(0) 154 | } 155 | 156 | // Sha256 - Hashes given arguments 157 | func Sha256(values ...string) string { 158 | hasher := sha256.New() 159 | for _, val := range values { 160 | hasher.Write([]byte(val)) 161 | } 162 | return hex.EncodeToString(hasher.Sum(nil)) 163 | } 164 | -------------------------------------------------------------------------------- /apps/server/src/email/mailer.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "html/template" 8 | "net/mail" 9 | "net/smtp" 10 | "net/url" 11 | "path/filepath" 12 | "runtime" 13 | 14 | "github.com/bananocoin/boompow/apps/server/src/config" 15 | "github.com/bananocoin/boompow/apps/server/src/models" 16 | "github.com/bananocoin/boompow/libs/utils" 17 | "k8s.io/klog/v2" 18 | ) 19 | 20 | // Returns path of an email template by name 21 | func getTemplatePath(name string) string { 22 | _, b, _, _ := runtime.Caller(0) 23 | basepath := filepath.Join(filepath.Dir(b), "templates", name) 24 | return basepath 25 | } 26 | 27 | // Send an email with given parameters 28 | func sendEmail(destination string, subject string, t *template.Template, templateData interface{}) error { 29 | // Get credentials 30 | smtpCredentials := utils.GetSmtpConnInformation() 31 | if smtpCredentials == nil { 32 | errMsg := "SMTP Credentials misconfigured, not sending email" 33 | klog.Errorf(errMsg) 34 | return fmt.Errorf("%s", errMsg) 35 | } 36 | 37 | // Send email 38 | auth := smtp.PlainAuth("", smtpCredentials.Username, smtpCredentials.Password, smtpCredentials.Server) 39 | from := mail.Address{ 40 | Name: "BoomPoW (Banano)", 41 | Address: "noreply@mail.banano.cc", 42 | } 43 | to := mail.Address{ 44 | Address: destination, 45 | } 46 | 47 | title := subject 48 | 49 | var body bytes.Buffer 50 | 51 | if err := t.ExecuteTemplate(&body, "base", templateData); err != nil { 52 | klog.Errorf("Error creating email template %s", err) 53 | return err 54 | } 55 | 56 | header := make(map[string]string) 57 | header["From"] = from.String() 58 | header["To"] = to.String() 59 | header["Subject"] = title 60 | header["MIME-Version"] = "1.0" 61 | header["Content-Type"] = "text/html; charset=\"utf-8\"" 62 | header["Content-Transfer-Encoding"] = "base64" 63 | 64 | message := "" 65 | for k, v := range header { 66 | message += fmt.Sprintf("%s: %s\r\n", k, v) 67 | } 68 | message += "\r\n" + base64.StdEncoding.EncodeToString(body.Bytes()) 69 | 70 | err := smtp.SendMail( 71 | fmt.Sprintf("%s:%d", smtpCredentials.Server, smtpCredentials.Port), 72 | auth, 73 | from.Address, 74 | []string{to.Address}, 75 | []byte(message), 76 | ) 77 | if err != nil { 78 | klog.Errorf("Error sending email %s", err) 79 | return err 80 | } 81 | 82 | return nil 83 | } 84 | 85 | // Load email template from file 86 | func loadEmailTemplate(templateName string) (*template.Template, error) { 87 | // Load template 88 | t, err := template.New("").ParseFiles(getTemplatePath(templateName), getTemplatePath("base.html")) 89 | if err != nil { 90 | klog.Errorf("Failed to load email template %s, %s", templateName, err) 91 | return nil, err 92 | } 93 | return t, nil 94 | } 95 | 96 | // Send email with link to verify user's email address 97 | func SendConfirmationEmail(destination string, userType models.UserType, token string) error { 98 | // Load template 99 | t, err := loadEmailTemplate("confirmemail.html") 100 | if err != nil { 101 | return err 102 | } 103 | 104 | // Populate template 105 | templateData := ConfirmationEmailData{ 106 | ConfirmationLink: fmt.Sprintf("https://boompow.banano.cc/verify_email/%s/%s", destination, token), 107 | ConfirmCodeExpirationDuration: config.EMAIL_CONFIRMATION_TOKEN_VALID_MINUTES, 108 | IsProvider: userType == models.PROVIDER, 109 | } 110 | 111 | return sendEmail( 112 | destination, 113 | "Confirm your email address for your BoomPOW Account", 114 | t, templateData, 115 | ) 116 | } 117 | 118 | // Send email with link to reset user's password 119 | func SendResetPasswordEmail(destination string, token string) error { 120 | // Load template 121 | t, err := loadEmailTemplate("resetpassword.html") 122 | if err != nil { 123 | return err 124 | } 125 | 126 | // Populate template 127 | templateData := ResetPasswordEmailData{ 128 | ResetPasswordLink: fmt.Sprintf("https://boompow.banano.cc/reset_password/%s", token), 129 | } 130 | 131 | return sendEmail( 132 | destination, 133 | "Reset the password for your BoomPoW Account", 134 | t, templateData, 135 | ) 136 | } 137 | 138 | // Send email with link to authorize service 139 | func SendAuthorizeServiceEmail(email string, name string, website string, token string) error { 140 | // Load template 141 | t, err := loadEmailTemplate("confirmservice.html") 142 | if err != nil { 143 | return err 144 | } 145 | 146 | // Encode URL params 147 | urlParam := url.QueryEscape(fmt.Sprintf(`query verifyService{ 148 | verifyService(input:{email:"%s", token:"%s"}) 149 | }`, email, token)) 150 | 151 | // Populate template 152 | templateData := ConfirmServiceEmailData{ 153 | ServiceName: name, 154 | EmailAddress: email, 155 | ServiceWebsite: website, 156 | ApproveServiceLink: fmt.Sprintf("https://boompow.banano.cc/graphql?query=%s", urlParam), 157 | } 158 | 159 | return sendEmail( 160 | "hello@appditto.com", 161 | "A service has requested access to BoomPoW", 162 | t, templateData, 163 | ) 164 | } 165 | 166 | // Send email letting service know they are approved 167 | func SendServiceApprovedEmail(email string) error { 168 | // Load template 169 | t, err := loadEmailTemplate("serviceapproved.html") 170 | if err != nil { 171 | return err 172 | } 173 | 174 | // Populate template 175 | templateData := map[string]string{} 176 | return sendEmail( 177 | email, 178 | "You have been authorized to use BoomPoW!", 179 | t, templateData, 180 | ) 181 | } 182 | -------------------------------------------------------------------------------- /apps/server/src/tests/work_repo_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/bananocoin/boompow/apps/server/src/database" 9 | "github.com/bananocoin/boompow/apps/server/src/repository" 10 | serializableModels "github.com/bananocoin/boompow/libs/models" 11 | utils "github.com/bananocoin/boompow/libs/utils/testing" 12 | ) 13 | 14 | // Test stats repo 15 | func TestStatsRepo(t *testing.T) { 16 | os.Setenv("MOCK_REDIS", "true") 17 | mockDb, err := database.NewConnection(&database.Config{ 18 | Host: os.Getenv("DB_MOCK_HOST"), 19 | Port: os.Getenv("DB_MOCK_PORT"), 20 | Password: os.Getenv("DB_MOCK_PASS"), 21 | User: os.Getenv("DB_MOCK_USER"), 22 | SSLMode: os.Getenv("DB_SSLMODE"), 23 | DBName: "testing", 24 | }) 25 | utils.AssertEqual(t, nil, err) 26 | err = database.DropAndCreateTables(mockDb) 27 | utils.AssertEqual(t, nil, err) 28 | userRepo := repository.NewUserService(mockDb) 29 | workRepo := repository.NewWorkService(mockDb, userRepo) 30 | 31 | // Create some users 32 | err = userRepo.CreateMockUsers() 33 | utils.AssertEqual(t, nil, err) 34 | 35 | providerEmail := "provider@gmail.com" 36 | requesterEmail := "requester@gmail.com" 37 | // Get users 38 | provider, _ := userRepo.GetUser(nil, &providerEmail) 39 | requester, _ := userRepo.GetUser(nil, &requesterEmail) 40 | 41 | _, err = workRepo.SaveOrUpdateWorkResult(repository.WorkMessage{ 42 | RequestedByEmail: requesterEmail, 43 | ProvidedByEmail: providerEmail, 44 | Hash: "123", 45 | Result: "ac", 46 | DifficultyMultiplier: 5, 47 | BlockAward: true, 48 | Precache: false, 49 | }) 50 | utils.AssertEqual(t, nil, err) 51 | _, err = workRepo.SaveOrUpdateWorkResult(repository.WorkMessage{ 52 | RequestedByEmail: requesterEmail, 53 | ProvidedByEmail: providerEmail, 54 | Hash: "566", 55 | Result: "ac", 56 | DifficultyMultiplier: 5, 57 | BlockAward: true, 58 | Precache: false, 59 | }) 60 | utils.AssertEqual(t, nil, err) 61 | _, err = workRepo.SaveOrUpdateWorkResult(repository.WorkMessage{ 62 | RequestedByEmail: providerEmail, 63 | ProvidedByEmail: requesterEmail, 64 | Hash: "321", 65 | Result: "ac", 66 | DifficultyMultiplier: 5, 67 | BlockAward: true, 68 | Precache: false, 69 | }) 70 | utils.AssertEqual(t, nil, err) 71 | 72 | workRequest, err := workRepo.GetWorkRecord("123") 73 | utils.AssertEqual(t, nil, err) 74 | utils.AssertEqual(t, workRequest.DifficultyMultiplier, 5) 75 | utils.AssertEqual(t, "ac", workRequest.Result) 76 | utils.AssertEqual(t, requester.ID, workRequest.RequestedBy) 77 | utils.AssertEqual(t, provider.ID, workRequest.ProvidedBy) 78 | 79 | // Get other stuff 80 | workDifficultySum, err := workRepo.GetUnpaidWorkSum() 81 | utils.AssertEqual(t, nil, err) 82 | utils.AssertEqual(t, 1500, workDifficultySum) 83 | workDifficultySumUser, err := workRepo.GetUnpaidWorkSumForUser(providerEmail) 84 | utils.AssertEqual(t, nil, err) 85 | utils.AssertEqual(t, 1000, workDifficultySumUser) 86 | 87 | // Test unpaid work group by 88 | workResults, err := workRepo.GetUnpaidWorkCountAndMarkAllPaid(mockDb) 89 | utils.AssertEqual(t, nil, err) 90 | utils.AssertEqual(t, 2, len(workResults)) 91 | for _, workResult := range workResults { 92 | if workResult.ProvidedBy == provider.ID { 93 | utils.AssertEqual(t, 2, workResult.UnpaidCount) 94 | utils.AssertEqual(t, 1000, workResult.DifficultySum) 95 | utils.AssertEqual(t, "ban_3bsnis6ha3m9cepuaywskn9jykdggxcu8mxsp76yc3oinrt3n7gi77xiggtm", workResult.BanAddress) 96 | } else { 97 | utils.AssertEqual(t, 1, workResult.UnpaidCount) 98 | utils.AssertEqual(t, 500, workResult.DifficultySum) 99 | } 100 | } 101 | 102 | // Test get top 10 103 | top10, err := workRepo.GetTopContributors(10) 104 | utils.AssertEqual(t, nil, err) 105 | for _, top := range top10 { 106 | utils.AssertEqual(t, "ban_3bsnis6ha3m9cepuaywskn9jykdggxcu8mxsp76yc3oinrt3n7gi77xiggtm", top.BanAddress) 107 | } 108 | 109 | // Test get services 110 | services, err := workRepo.GetServiceStats() 111 | utils.AssertEqual(t, nil, err) 112 | utils.AssertEqual(t, 2, len(services)) 113 | utils.AssertEqual(t, 2, services[0].TotalRequests) 114 | utils.AssertEqual(t, "https://service.com", services[0].ServiceWebsite) 115 | utils.AssertEqual(t, "Service Name", services[0].ServiceName) 116 | utils.AssertEqual(t, 1, services[1].TotalRequests) 117 | 118 | // Test the worker 119 | statsChan := make(chan repository.WorkMessage, 100) 120 | blockAwardedChan := make(chan serializableModels.ClientMessage, 100) 121 | 122 | // Stats stats processing job 123 | go workRepo.StatsWorker(statsChan, &blockAwardedChan) 124 | 125 | statsChan <- repository.WorkMessage{ 126 | RequestedByEmail: requesterEmail, 127 | ProvidedByEmail: providerEmail, 128 | Hash: "321", 129 | Result: "fe", 130 | DifficultyMultiplier: 3, 131 | BlockAward: true, 132 | Precache: false, 133 | } 134 | 135 | time.Sleep(1 * time.Second) // Arbitrary time to wait for the worker to process the message 136 | workRequest, err = workRepo.GetWorkRecord("321") 137 | utils.AssertEqual(t, nil, err) 138 | utils.AssertEqual(t, workRequest.DifficultyMultiplier, 3) 139 | utils.AssertEqual(t, "fe", workRequest.Result) 140 | utils.AssertEqual(t, requester.ID, workRequest.RequestedBy) 141 | utils.AssertEqual(t, provider.ID, workRequest.ProvidedBy) 142 | utils.AssertEqual(t, 1, len(blockAwardedChan)) 143 | } 144 | -------------------------------------------------------------------------------- /libs/utils/ed25519/ed25519.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package ed25519 implements the Ed25519 signature algorithm. See 6 | // https://ed25519.cr.yp.to/. 7 | // 8 | // These functions are also compatible with the “Ed25519” function defined in 9 | // RFC 8032. 10 | package ed25519 11 | 12 | // This code is a port of the public domain, “ref10” implementation of ed25519 13 | // from SUPERCOP. 14 | 15 | import ( 16 | "bytes" 17 | "crypto" 18 | cryptorand "crypto/rand" 19 | "errors" 20 | "io" 21 | "strconv" 22 | 23 | "github.com/bananocoin/boompow/libs/utils/ed25519/edwards25519" 24 | "golang.org/x/crypto/blake2b" 25 | ) 26 | 27 | const ( 28 | // PublicKeySize is the size, in bytes, of public keys as used in this package. 29 | PublicKeySize = 32 30 | // PrivateKeySize is the size, in bytes, of private keys as used in this package. 31 | PrivateKeySize = 64 32 | // SignatureSize is the size, in bytes, of signatures generated and verified by this package. 33 | SignatureSize = 64 34 | ) 35 | 36 | // PublicKey is the type of Ed25519 public keys. 37 | type PublicKey []byte 38 | 39 | // PrivateKey is the type of Ed25519 private keys. It implements crypto.Signer. 40 | type PrivateKey []byte 41 | 42 | // Public returns the PublicKey corresponding to priv. 43 | func (priv PrivateKey) Public() crypto.PublicKey { 44 | publicKey := make([]byte, PublicKeySize) 45 | copy(publicKey, priv[32:]) 46 | return PublicKey(publicKey) 47 | } 48 | 49 | // Sign signs the given message with priv. 50 | // Ed25519 performs two passes over messages to be signed and therefore cannot 51 | // handle pre-hashed messages. Thus opts.HashFunc() must return zero to 52 | // indicate the message hasn't been hashed. This can be achieved by passing 53 | // crypto.Hash(0) as the value for opts. 54 | func (priv PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) { 55 | if opts.HashFunc() != crypto.Hash(0) { 56 | return nil, errors.New("ed25519: cannot sign hashed message") 57 | } 58 | 59 | return Sign(priv, message), nil 60 | } 61 | 62 | // GenerateKey generates a public/private key pair using entropy from rand. 63 | // If rand is nil, crypto/rand.Reader will be used. 64 | func GenerateKey(rand io.Reader) (publicKey PublicKey, privateKey PrivateKey, err error) { 65 | if rand == nil { 66 | rand = cryptorand.Reader 67 | } 68 | 69 | privateKey = make([]byte, PrivateKeySize) 70 | publicKey = make([]byte, PublicKeySize) 71 | _, err = io.ReadFull(rand, privateKey[:32]) 72 | if err != nil { 73 | return nil, nil, err 74 | } 75 | 76 | digest := blake2b.Sum512(privateKey[:32]) 77 | digest[0] &= 248 78 | digest[31] &= 127 79 | digest[31] |= 64 80 | 81 | var A edwards25519.ExtendedGroupElement 82 | var hBytes [32]byte 83 | copy(hBytes[:], digest[:]) 84 | edwards25519.GeScalarMultBase(&A, &hBytes) 85 | var publicKeyBytes [32]byte 86 | A.ToBytes(&publicKeyBytes) 87 | 88 | copy(privateKey[32:], publicKeyBytes[:]) 89 | copy(publicKey, publicKeyBytes[:]) 90 | 91 | return publicKey, privateKey, nil 92 | } 93 | 94 | // Sign signs the message with privateKey and returns a signature. It will 95 | // panic if len(privateKey) is not PrivateKeySize. 96 | func Sign(privateKey PrivateKey, message []byte) []byte { 97 | if l := len(privateKey); l != PrivateKeySize { 98 | panic("ed25519: bad private key length: " + strconv.Itoa(l)) 99 | } 100 | 101 | h, _ := blake2b.New512(nil) 102 | h.Write(privateKey[:32]) 103 | 104 | var digest1, messageDigest, hramDigest [64]byte 105 | var expandedSecretKey [32]byte 106 | h.Sum(digest1[:0]) 107 | copy(expandedSecretKey[:], digest1[:]) 108 | expandedSecretKey[0] &= 248 109 | expandedSecretKey[31] &= 63 110 | expandedSecretKey[31] |= 64 111 | 112 | h.Reset() 113 | h.Write(digest1[32:]) 114 | h.Write(message) 115 | h.Sum(messageDigest[:0]) 116 | 117 | var messageDigestReduced [32]byte 118 | edwards25519.ScReduce(&messageDigestReduced, &messageDigest) 119 | var R edwards25519.ExtendedGroupElement 120 | edwards25519.GeScalarMultBase(&R, &messageDigestReduced) 121 | 122 | var encodedR [32]byte 123 | R.ToBytes(&encodedR) 124 | 125 | h.Reset() 126 | h.Write(encodedR[:]) 127 | h.Write(privateKey[32:]) 128 | h.Write(message) 129 | h.Sum(hramDigest[:0]) 130 | var hramDigestReduced [32]byte 131 | edwards25519.ScReduce(&hramDigestReduced, &hramDigest) 132 | 133 | var s [32]byte 134 | edwards25519.ScMulAdd(&s, &hramDigestReduced, &expandedSecretKey, &messageDigestReduced) 135 | 136 | signature := make([]byte, SignatureSize) 137 | copy(signature[:], encodedR[:]) 138 | copy(signature[32:], s[:]) 139 | 140 | return signature 141 | } 142 | 143 | // Verify reports whether sig is a valid signature of message by publicKey. It 144 | // will panic if len(publicKey) is not PublicKeySize. 145 | func Verify(publicKey PublicKey, message, sig []byte) bool { 146 | if l := len(publicKey); l != PublicKeySize { 147 | panic("ed25519: bad public key length: " + strconv.Itoa(l)) 148 | } 149 | 150 | if len(sig) != SignatureSize || sig[63]&224 != 0 { 151 | return false 152 | } 153 | 154 | var A edwards25519.ExtendedGroupElement 155 | var publicKeyBytes [32]byte 156 | copy(publicKeyBytes[:], publicKey) 157 | if !A.FromBytes(&publicKeyBytes) { 158 | return false 159 | } 160 | edwards25519.FeNeg(&A.X, &A.X) 161 | edwards25519.FeNeg(&A.T, &A.T) 162 | 163 | h, _ := blake2b.New512(nil) 164 | h.Write(sig[:32]) 165 | h.Write(publicKey[:]) 166 | h.Write(message) 167 | var digest [64]byte 168 | h.Sum(digest[:0]) 169 | 170 | var hReduced [32]byte 171 | edwards25519.ScReduce(&hReduced, &digest) 172 | 173 | var R edwards25519.ProjectiveGroupElement 174 | var b [32]byte 175 | copy(b[:], sig[32:]) 176 | edwards25519.GeDoubleScalarMultVartime(&R, &hReduced, &A, &b) 177 | 178 | var checkR [32]byte 179 | R.ToBytes(&checkR) 180 | return bytes.Equal(sig[:32], checkR[:]) 181 | } 182 | -------------------------------------------------------------------------------- /apps/server/src/tests/auth_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | // func TestAuthMiddleware(t *testing.T) { 4 | // os.Setenv("MOCK_REDIS", "true") 5 | // mockDb, err := database.NewConnection(&database.Config{ 6 | // Host: os.Getenv("DB_MOCK_HOST"), 7 | // Port: os.Getenv("DB_MOCK_PORT"), 8 | // Password: os.Getenv("DB_MOCK_PASS"), 9 | // User: os.Getenv("DB_MOCK_USER"), 10 | // SSLMode: os.Getenv("DB_SSLMODE"), 11 | // DBName: "testing", 12 | // }) 13 | // utils.AssertEqual(t, nil, err) 14 | // err = database.DropAndCreateTables(mockDb) 15 | // utils.AssertEqual(t, nil, err) 16 | // userRepo := repository.NewUserService(mockDb) 17 | // userRepo.CreateMockUsers() 18 | 19 | // publicEndpoint := func(w http.ResponseWriter, r *http.Request) { 20 | // w.WriteHeader(http.StatusOK) 21 | // w.Write([]byte("banano")) 22 | // } 23 | 24 | // // Endpoint that requires auth from the people that provide work 25 | // authorizedProviderEndpoint := func(w http.ResponseWriter, r *http.Request) { 26 | // // Only PROVIDER type users can provide work 27 | // provider := middleware.AuthorizedProvider(r.Context()) 28 | // if provider == nil { 29 | // w.WriteHeader(http.StatusUnauthorized) 30 | // w.Write([]byte("401 - Unauthorized")) 31 | // return 32 | // } 33 | // w.WriteHeader(http.StatusOK) 34 | // w.Write([]byte("banano")) 35 | // } 36 | 37 | // // Endpoint that requires auth from the people that request work 38 | // authorizedRequesterEndpoint := func(w http.ResponseWriter, r *http.Request) { 39 | // // Only REQUESTER type users can provide work 40 | // requester := middleware.AuthorizedRequester(r.Context()) 41 | // if requester == nil { 42 | // w.WriteHeader(http.StatusUnauthorized) 43 | // w.Write([]byte("401 - Unauthorized")) 44 | // return 45 | // } 46 | // w.WriteHeader(http.StatusOK) 47 | // w.Write([]byte("banano")) 48 | // } 49 | 50 | // // Endpoint that requires a token 51 | // authorizedTokenEndpoint := func(w http.ResponseWriter, r *http.Request) { 52 | // // Only tokens work 53 | // requester := middleware.AuthorizedServiceToken(r.Context()) 54 | // if requester == nil { 55 | // w.WriteHeader(http.StatusUnauthorized) 56 | // w.Write([]byte("401 - Unauthorized")) 57 | // return 58 | // } 59 | // w.WriteHeader(http.StatusOK) 60 | // w.Write([]byte("banano")) 61 | // } 62 | 63 | // authMiddleware := middleware.AuthMiddleware(userRepo) 64 | // router := chi.NewRouter() 65 | // router.Use(authMiddleware) 66 | // router.Get("/", publicEndpoint) 67 | // router.Get("/authProvider", authorizedProviderEndpoint) 68 | // router.Get("/authRequester", authorizedRequesterEndpoint) 69 | // router.Get("/authToken", authorizedTokenEndpoint) 70 | // ts := httptest.NewServer(router) 71 | // defer ts.Close() 72 | 73 | // // Test that middleware doesn't block public endpoints 74 | // if resp, body := testRequest(t, ts, "GET", "/", nil, ""); body != "banano" && resp.StatusCode != http.StatusOK { 75 | // t.Fatalf(body) 76 | // } 77 | 78 | // // Test private jwt endpoint is blocked when not authorized 79 | // if resp, body := testRequest(t, ts, "GET", "/authProvider", nil, ""); resp.StatusCode != http.StatusUnauthorized { 80 | // t.Fatalf(body) 81 | // } 82 | 83 | // // Get a JWT token 84 | // providerToken, _ := auth.GenerateToken("provider@gmail.com", time.Now) 85 | // requesterToken, _ := auth.GenerateToken("requester@gmail.com", time.Now) 86 | 87 | // // Endpoint that is only good for providers 88 | 89 | // // Test private jwt endpoint is not blocked when authorized 90 | // if resp, body := testRequest(t, ts, "GET", "/authProvider", nil, providerToken); resp.StatusCode != http.StatusOK && body != "banano" { 91 | // t.Fatalf(body) 92 | // } 93 | // // Test private jwt endpoint is blocked for requester 94 | // if resp, body := testRequest(t, ts, "GET", "/authProvider", nil, requesterToken); resp.StatusCode != http.StatusUnauthorized { 95 | // t.Fatalf(body) 96 | // } 97 | 98 | // // Endpoint that is only good for requesters 99 | 100 | // // Test private jwt endpoint is not blocked when authorized 101 | // if resp, body := testRequest(t, ts, "GET", "/authRequester", nil, requesterToken); resp.StatusCode != http.StatusOK && body != "banano" { 102 | // t.Fatalf(body) 103 | // } 104 | // // Test private jwt endpoint is blocked for provider 105 | // if resp, body := testRequest(t, ts, "GET", "/authRequester", nil, providerToken); resp.StatusCode != http.StatusUnauthorized { 106 | // t.Fatalf(body) 107 | // } 108 | 109 | // // Endpoint that is only good for tokens (not jwt) 110 | // if resp, body := testRequest(t, ts, "GET", "/authToken", nil, providerToken); resp.StatusCode != http.StatusUnauthorized { 111 | // t.Fatalf(body) 112 | // } 113 | // if resp, body := testRequest(t, ts, "GET", "/authToken", nil, requesterToken); resp.StatusCode != http.StatusUnauthorized { 114 | // t.Fatalf(body) 115 | // } 116 | // // Create the token 117 | // // Generate token 118 | // token := userRepo.GenerateServiceToken() 119 | // requesterEmail := "requester@gmail.com" 120 | // // Get user 121 | // requester, _ := userRepo.GetUser(nil, &requesterEmail) 122 | // if err := database.GetRedisDB().AddServiceToken(requester.ID, token); err != nil { 123 | // t.Errorf("Error adding service token to redis: %s", err.Error()) 124 | // } 125 | 126 | // if resp, body := testRequest(t, ts, "GET", "/authToken", nil, token); resp.StatusCode != http.StatusOK && body != "banano" { 127 | // t.Fatalf(body) 128 | // } 129 | // // Test a random token 130 | // if resp, body := testRequest(t, ts, "GET", "/authToken", nil, userRepo.GenerateServiceToken()); resp.StatusCode != http.StatusForbidden { 131 | // t.Fatalf(body) 132 | // } 133 | // } 134 | 135 | // func testRequest(t *testing.T, ts *httptest.Server, method, path string, body io.Reader, token string) (*http.Response, string) { 136 | // req, err := http.NewRequest(method, ts.URL+path, body) 137 | // if token != "" { 138 | // req.Header.Set("Authorization", token) 139 | // } 140 | // if err != nil { 141 | // t.Fatal(err) 142 | // return nil, "" 143 | // } 144 | 145 | // resp, err := http.DefaultClient.Do(req) 146 | // if err != nil { 147 | // t.Fatal(err) 148 | // return nil, "" 149 | // } 150 | 151 | // respBody, err := ioutil.ReadAll(resp.Body) 152 | // if err != nil { 153 | // t.Fatal(err) 154 | // return nil, "" 155 | // } 156 | // defer resp.Body.Close() 157 | 158 | // return resp, string(respBody) 159 | // } 160 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 💫 CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | test: 9 | name: ☔️ Tests 10 | runs-on: ubuntu-latest 11 | container: golang:1.19 12 | 13 | # Setup postgres service for tests 14 | services: 15 | db: 16 | image: postgres:14 17 | env: 18 | POSTGRES_DB: testing 19 | POSTGRES_PASSWORD: postgres 20 | POSTGRES_USER: postgres 21 | ports: 22 | - 5432:5432 23 | # set health checks to wait until postgres has started 24 | options: >- 25 | --health-cmd pg_isready 26 | --health-interval 10s 27 | --health-timeout 5s 28 | --health-retries 5 29 | 30 | steps: 31 | - name: Check out code 32 | uses: actions/checkout@master 33 | 34 | - name: Run Tests 35 | env: 36 | DB_MOCK_HOST: db 37 | DB_MOCK_PORT: 5432 38 | DB_MOCK_USER: postgres 39 | DB_MOCK_PASS: postgres 40 | DB_SSLMODE: disable 41 | run: | 42 | go test -v -parallel 1 $(go list -f '{{.Dir}}/...' -m | xargs) 43 | 44 | build_and_publish: 45 | name: ⚒️ Build and Publish Server 46 | needs: test 47 | runs-on: ubuntu-latest 48 | env: 49 | GITHUB_RUN_ID: ${{ github.run_id }} 50 | steps: 51 | - uses: actions/checkout@master 52 | 53 | - name: Get branch name (merge) 54 | if: github.event_name != 'pull_request' 55 | shell: bash 56 | run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/} | tr / -)" >> $GITHUB_ENV 57 | 58 | - name: Get branch name (pull request) 59 | if: github.event_name == 'pull_request' 60 | shell: bash 61 | run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | tr / -)" >> $GITHUB_ENV 62 | 63 | - name: Login to registry 64 | if: success() 65 | uses: docker/login-action@v2 66 | with: 67 | username: ${{ secrets.DOCKER_USER }} 68 | password: ${{ secrets.DOCKER_PASSWORD }} 69 | 70 | - name: Build and push 71 | if: success() 72 | uses: docker/build-push-action@v3 73 | with: 74 | context: . 75 | platforms: linux/amd64 76 | push: true 77 | file: ./apps/server/Dockerfile 78 | tags: bananocoin/boompow-next:${{ env.BRANCH_NAME }}-${{ env.GITHUB_RUN_ID }} 79 | 80 | build_and_publish_moneybags: 81 | name: ⚒️ Build and Publish Moneybags 82 | needs: test 83 | runs-on: ubuntu-latest 84 | env: 85 | GITHUB_RUN_ID: ${{ github.run_id }} 86 | steps: 87 | - uses: actions/checkout@master 88 | 89 | - name: Get branch name (merge) 90 | if: github.event_name != 'pull_request' 91 | shell: bash 92 | run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/} | tr / -)" >> $GITHUB_ENV 93 | 94 | - name: Get branch name (pull request) 95 | if: github.event_name == 'pull_request' 96 | shell: bash 97 | run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | tr / -)" >> $GITHUB_ENV 98 | 99 | - name: Login to registry 100 | if: success() 101 | uses: docker/login-action@v2 102 | with: 103 | username: ${{ secrets.DOCKER_USER }} 104 | password: ${{ secrets.DOCKER_PASSWORD }} 105 | 106 | - name: Build and push 107 | if: success() 108 | uses: docker/build-push-action@v3 109 | with: 110 | context: . 111 | platforms: linux/amd64 112 | push: true 113 | file: ./services/moneybags/Dockerfile 114 | tags: bananocoin/boompow-payments:${{ env.BRANCH_NAME }}-${{ env.GITHUB_RUN_ID }} 115 | 116 | deploy_go: 117 | name: 🥳 Deploy Server 118 | needs: build_and_publish 119 | runs-on: ubuntu-latest 120 | env: 121 | GITHUB_RUN_ID: ${{ github.run_id }} 122 | steps: 123 | - uses: actions/checkout@master 124 | - uses: imranismail/setup-kustomize@v1 125 | with: 126 | kustomize-version: "3.5.4" 127 | 128 | - name: Get branch name (merge) 129 | if: github.event_name != 'pull_request' 130 | shell: bash 131 | run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/} | tr / -)" >> $GITHUB_ENV 132 | 133 | - name: Get branch name (pull request) 134 | if: github.event_name == 'pull_request' 135 | shell: bash 136 | run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | tr / -)" >> $GITHUB_ENV 137 | 138 | - name: Set image 139 | working-directory: ./kubernetes 140 | run: | 141 | kustomize edit set image replaceme=bananocoin/boompow-next:${{ env.BRANCH_NAME }}-${{ env.GITHUB_RUN_ID }} 142 | kustomize build . > go-deployment.yaml 143 | - name: Deploy image to k8s cluster 144 | uses: bbedward/kubectl@master 145 | env: 146 | KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} 147 | with: 148 | args: apply -f ./kubernetes/go-deployment.yaml 149 | 150 | deploy_cron: 151 | name: 💰 Deploy Moneybags 152 | needs: build_and_publish_moneybags 153 | runs-on: ubuntu-latest 154 | env: 155 | GITHUB_RUN_ID: ${{ github.run_id }} 156 | steps: 157 | - uses: actions/checkout@master 158 | - uses: imranismail/setup-kustomize@v1 159 | with: 160 | kustomize-version: "3.5.4" 161 | 162 | - name: Get branch name (merge) 163 | if: github.event_name != 'pull_request' 164 | shell: bash 165 | run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/} | tr / -)" >> $GITHUB_ENV 166 | 167 | - name: Get branch name (pull request) 168 | if: github.event_name == 'pull_request' 169 | shell: bash 170 | run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | tr / -)" >> $GITHUB_ENV 171 | 172 | - name: Set image 173 | working-directory: ./kubernetes/moneybags 174 | run: | 175 | kustomize edit set image replaceme=bananocoin/boompow-payments:${{ env.BRANCH_NAME }}-${{ env.GITHUB_RUN_ID }} 176 | kustomize build . > cron-deployment.yaml 177 | - name: Deploy image to k8s cluster 178 | uses: bbedward/kubectl@master 179 | env: 180 | KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }} 181 | with: 182 | args: apply -f ./kubernetes/moneybags/cron-deployment.yaml -------------------------------------------------------------------------------- /apps/server/src/email/templates/serviceapproved.html: -------------------------------------------------------------------------------- 1 | {{define "body"}} 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 34 | 35 | 36 | 37 | 38 | 39 | 58 | 59 | 60 | 61 | 62 | 63 | 124 | 125 | 126 | 127 | 128 | 129 | 152 | 153 | 154 | 155 |
14 | 19 | 20 | 21 | 26 | 27 |
22 | 23 | Logo 24 | 25 |
28 | 33 |
40 | 45 | 46 | 47 | 50 | 51 |
48 |

You have been approved to use BoomPoW!

49 |
52 | 57 |
64 | 69 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 81 | 82 | 83 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 106 | 107 | 108 | 109 | 110 | 111 | 114 | 115 | 116 | 117 |
74 |

You have been granted access to BoomPoW!

75 |
79 |

You can retrieve a token by logging into the website, boompow.banano.cc.

80 |
84 |

The token is used in an "Authorization" header with the `workGenerate` mutation. If you are using Pippin, simply add BPOW_KEY to your environment with the generated token.

85 |

Send HTTP post requests to https://boompow.banano.cc/graphql with the following body.

86 |

87 |

 88 |               
 89 | mutation($hash:String!, $difficultyMultiplier: Int!) {
 90 |     workGenerate(input:{hash:$hash, difficultyMultiplier:$difficultyMultiplier})
 91 | }
 92 |               
 93 |             
94 |

Difficulty multiplier begins at 1, which is Nano's receive difficulty and Banano's difficulty. 64 is equivalent to Nano's send difficulty.

95 |

96 |
104 |

Pippin, the easiest way to use BoomPoW

105 |
112 |

Benis,
The Banano Team

113 |
118 | 123 |
130 | 135 | 136 | 137 | 138 | 139 | 142 | 143 | 144 | 145 |
140 |

You received this email because you have requested access to use BoomPoW

141 |
146 | 151 |
156 | 157 | {{end}} 158 | -------------------------------------------------------------------------------- /apps/server/src/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/99designs/gqlgen/graphql" 10 | "github.com/bananocoin/boompow/apps/server/src/database" 11 | "github.com/bananocoin/boompow/apps/server/src/models" 12 | "github.com/bananocoin/boompow/apps/server/src/repository" 13 | "github.com/bananocoin/boompow/libs/utils" 14 | "github.com/bananocoin/boompow/libs/utils/auth" 15 | "github.com/bananocoin/boompow/libs/utils/net" 16 | "github.com/google/uuid" 17 | "golang.org/x/exp/slices" 18 | "k8s.io/klog/v2" 19 | ) 20 | 21 | // We distinguish the type of authentication so we can restrict service tokens to only be used for work requests 22 | type UserContextValue struct { 23 | User *models.User 24 | AuthType string 25 | } 26 | 27 | var userCtxKey = &contextKey{"user"} 28 | 29 | type contextKey struct { 30 | name string 31 | } 32 | 33 | func formatGraphqlError(ctx context.Context, msg string) string { 34 | marshalled, err := json.Marshal(graphql.ErrorResponse(ctx, "Invalid token")) 35 | if err != nil { 36 | return "\"errors\": [{\"message\": \"Unknown\"}]" 37 | } 38 | return string(marshalled) 39 | } 40 | 41 | func AuthMiddleware(userRepo *repository.UserService) func(http.Handler) http.Handler { 42 | return func(next http.Handler) http.Handler { 43 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 44 | // There are two types of tokens 45 | // The first is a JWT token that is used to authenticate users 46 | // The second is an "application" token that is used to authenticate services (no expiry) 47 | header := r.Header.Get("Authorization") 48 | 49 | // Allow unauthenticated users in 50 | if header == "" { 51 | next.ServeHTTP(w, r) 52 | return 53 | } 54 | 55 | var ctx context.Context 56 | 57 | // Determine token type 58 | if strings.HasPrefix(header, "resetpassword:") { 59 | token := header[len("resetpassword:"):] 60 | email, err := auth.ParseToken(token) 61 | if err != nil { 62 | http.Error(w, formatGraphqlError(r.Context(), "Invalid Token"), http.StatusForbidden) 63 | return 64 | } 65 | // Get from redis 66 | _, err = database.GetRedisDB().GetResetPasswordToken(email) 67 | if err != nil { 68 | http.Error(w, formatGraphqlError(r.Context(), "Invalid Token"), http.StatusForbidden) 69 | return 70 | } 71 | // create user and check if user exists in db 72 | user, err := userRepo.GetUser(nil, &email) 73 | if err != nil { 74 | next.ServeHTTP(w, r) 75 | return 76 | } 77 | // put it in context 78 | ctx = context.WithValue(r.Context(), userCtxKey, &UserContextValue{User: user, AuthType: "token"}) 79 | } else if strings.HasPrefix(header, "service:") { 80 | // Service token 81 | if !slices.Contains(utils.GetServiceTokens(), header) { 82 | klog.Errorf("INVALID TOKEN ATTEMPT 1 %s:%s", header, net.GetIPAddress(r)) 83 | http.Error(w, formatGraphqlError(r.Context(), "Invalid Token"), http.StatusForbidden) 84 | return 85 | } 86 | userID, err := database.GetRedisDB().GetServiceTokenUser(header) 87 | if err != nil { 88 | klog.Errorf("INVALID TOKEN ATTEMPT %s:%s", header, net.GetIPAddress(r)) 89 | http.Error(w, formatGraphqlError(r.Context(), "Invalid Token"), http.StatusForbidden) 90 | return 91 | } 92 | userUUID, err := uuid.Parse(userID) 93 | if err != nil { 94 | http.Error(w, formatGraphqlError(r.Context(), "Invalid Token"), http.StatusForbidden) 95 | return 96 | } 97 | // create user and check if user exists in db 98 | user, err := userRepo.GetUser(&userUUID, nil) 99 | if err != nil || user.Banned { 100 | next.ServeHTTP(w, r) 101 | return 102 | } 103 | // put it in context 104 | ctx = context.WithValue(r.Context(), userCtxKey, &UserContextValue{User: user, AuthType: "token"}) 105 | } else { 106 | tokenStr := header 107 | email, err := auth.ParseToken(tokenStr) 108 | if err != nil { 109 | http.Error(w, formatGraphqlError(r.Context(), "Invalid Token"), http.StatusForbidden) 110 | return 111 | } 112 | // create user and check if user exists in db 113 | user, err := userRepo.GetUser(nil, &email) 114 | if err != nil { 115 | next.ServeHTTP(w, r) 116 | return 117 | } 118 | // put it in context 119 | ctx = context.WithValue(r.Context(), userCtxKey, &UserContextValue{User: user, AuthType: "jwt"}) 120 | 121 | } 122 | 123 | // and call the next with our new context 124 | r = r.WithContext(ctx) 125 | next.ServeHTTP(w, r) 126 | }) 127 | } 128 | } 129 | 130 | // forContext finds the user from the context. REQUIRES Middleware to have run. 131 | func forContext(ctx context.Context) *UserContextValue { 132 | raw, _ := ctx.Value(userCtxKey).(*UserContextValue) 133 | return raw 134 | } 135 | 136 | // AuthorizedUser returns user from context if they are logged in 137 | func AuthorizedUser(ctx context.Context) *UserContextValue { 138 | contextValue := forContext(ctx) 139 | if contextValue == nil || contextValue.User == nil || contextValue.AuthType != "jwt" { 140 | return nil 141 | } 142 | return contextValue 143 | } 144 | 145 | // AuthorizedProvider returns user from context if they are an authorized provider type 146 | func AuthorizedProvider(ctx context.Context) *UserContextValue { 147 | contextValue := forContext(ctx) 148 | if contextValue == nil || contextValue.User == nil || contextValue.AuthType != "jwt" || !contextValue.User.EmailVerified || contextValue.User.Type != models.PROVIDER { 149 | return nil 150 | } 151 | return contextValue 152 | } 153 | 154 | // AuthorizedRequester returns user from context if they are an authorized requester 155 | func AuthorizedRequester(ctx context.Context) *UserContextValue { 156 | contextValue := forContext(ctx) 157 | if contextValue == nil || contextValue.User == nil || contextValue.AuthType != "jwt" || !contextValue.User.EmailVerified || !contextValue.User.CanRequestWork || contextValue.User.Type != models.REQUESTER { 158 | return nil 159 | } 160 | return contextValue 161 | } 162 | 163 | // AuthorizedServiceToken returns user from context if they are an authorized service token 164 | func AuthorizedServiceToken(ctx context.Context) *UserContextValue { 165 | contextValue := forContext(ctx) 166 | if contextValue == nil || contextValue.User == nil || contextValue.AuthType != "token" || !contextValue.User.EmailVerified || !contextValue.User.CanRequestWork || contextValue.User.Type != models.REQUESTER { 167 | return nil 168 | } 169 | return contextValue 170 | } 171 | 172 | // AuthorizedChangePassword getsuser from context if they are authorized to change their password 173 | func AuthorizedChangePassword(ctx context.Context) *UserContextValue { 174 | contextValue := forContext(ctx) 175 | if contextValue == nil || contextValue.User == nil || contextValue.AuthType != "token" { 176 | return nil 177 | } 178 | return contextValue 179 | } 180 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/bbedward/nanopow v0.0.0-20220813150113-8e6381d86476 h1:djatQVcJDwh7ojCMh4G+StXJAnuP/9/Y+WWZljbFcog= 2 | github.com/bbedward/nanopow v0.0.0-20220813150113-8e6381d86476/go.mod h1:LlfiFzkzg1LQa16NfOIodEcj35rntNnpCgIswbsD/Kw= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 5 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 6 | github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= 7 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 8 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 9 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 10 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 11 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 12 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 13 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 14 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 15 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= 16 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 17 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 18 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 19 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 20 | github.com/hasura/go-graphql-client v0.7.2/go.mod h1:NVifIwv+YFIUYGLQ7SM2/vBbzS/9rFP4vmIf/vf/zXM= 21 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 22 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 23 | github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 24 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 25 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 26 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 27 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 28 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 29 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 30 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 31 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 32 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 33 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 34 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 35 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 36 | github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= 37 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 38 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 39 | github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= 40 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 41 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 42 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 43 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 44 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 45 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 46 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 47 | golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 48 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 49 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 51 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 52 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= 59 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 60 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 61 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 62 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 63 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 64 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 65 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 66 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 67 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 68 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 69 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 70 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 71 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 72 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 73 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 74 | -------------------------------------------------------------------------------- /apps/server/src/email/templates/resetpassword.html: -------------------------------------------------------------------------------- 1 | {{define "body"}} 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 34 | 35 | 36 | 37 | 38 | 39 | 58 | 59 | 60 | 61 | 62 | 63 | 130 | 131 | 132 | 133 | 134 | 135 | 158 | 159 | 160 | 161 |
14 | 19 | 20 | 21 | 26 | 27 |
22 | 23 | Logo 24 | 25 |
28 | 33 |
40 | 45 | 46 | 47 | 50 | 51 |
48 |

Reset Password

49 |
52 | 57 |
64 | 69 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 79 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 103 | 104 | 105 | 106 | 107 | 108 | 112 | 113 | 114 | 115 | 116 | 117 | 120 | 121 | 122 | 123 |
74 |

We recently received a request to reset your password for BoomPoW, if you did not make this request - you may delete this email.

75 |
80 |

Click the link below to reset your password.

81 |
89 | 90 | 91 | 100 | 101 |
92 | 93 | 94 | 97 | 98 |
95 | Reset Password 96 |
99 |
102 |
109 |

If that doesn't work, copy and paste the following link in your browser:

110 |

{{ .ResetPasswordLink }}

111 |
118 |

Benis,
The Banano Team

119 |
124 | 129 |
136 | 141 | 142 | 143 | 144 | 145 | 148 | 149 | 150 | 151 |
146 |

You received this email because we received a request to reset the password for your BoomPoW account. If you didn't make this request, you may delete this email.

147 |
152 | 157 |
162 | 163 | {{end}} --------------------------------------------------------------------------------