├── docs ├── index.html └── version.json ├── .gitignore ├── webserver ├── public │ ├── robots.txt │ ├── static │ │ └── media │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ └── favicon-32x32.png │ ├── manifest.json │ └── index.html ├── src │ ├── logo512.png │ ├── toaster.js │ ├── settings │ │ ├── index.js │ │ ├── bakersettings.js │ │ ├── rpcservers.js │ │ └── notifications.js │ ├── nextopportunities.js │ ├── util.js │ ├── index.css │ ├── dashboard.js │ ├── wizards │ │ ├── index.js │ │ ├── ledger.js │ │ └── wallet.js │ ├── index.js │ ├── delegateregister.js │ ├── delegateinfo.js │ └── voting.js ├── .gitignore ├── package.json ├── api_voting.go ├── api.go ├── api_payouts.go ├── api_wizard.go ├── api_settings.go └── webserver.go ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── build.yml ├── Dockerfile ├── nonce └── nonce_types.go ├── .golangci.yml ├── go.mod ├── storage ├── notifications.go ├── watermark.go ├── nonce.go ├── storage.go ├── rights.go └── config.go ├── util ├── util.go └── networkconstants.go ├── logging.go ├── notifications ├── email.go ├── telegram.go └── notifications.go ├── Makefile ├── baconclient └── baconstatus.go ├── README.md ├── versioncheck.go ├── baconsigner ├── wallet.go ├── crypto.go ├── ledger.go └── baconsigner.go ├── payouts ├── delegator_reward.go └── cycle_rewards_metadata.go ├── bakinbacon_test.go ├── nonce.go ├── endorsing.go ├── bakinbacon.go └── prefetch.go /docs/index.html: -------------------------------------------------------------------------------- 1 | Hi. BakinBacon 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bakinbacon 2 | bakinbacon-* 3 | bakinbacon.db 4 | log-bakinbacon-* 5 | -------------------------------------------------------------------------------- /webserver/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /webserver/src/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakingbacon/bakinbacon/HEAD/webserver/src/logo512.png -------------------------------------------------------------------------------- /webserver/public/static/media/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakingbacon/bakinbacon/HEAD/webserver/public/static/media/favicon.ico -------------------------------------------------------------------------------- /webserver/public/static/media/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakingbacon/bakinbacon/HEAD/webserver/public/static/media/favicon-16x16.png -------------------------------------------------------------------------------- /webserver/public/static/media/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakingbacon/bakinbacon/HEAD/webserver/public/static/media/favicon-32x32.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Node stuff downloaded anyways 2 | webserver/node_modules 3 | 4 | # Ignore logs 5 | log-bakinbacon-*.log 6 | 7 | # Not the database 8 | bakinbacon.db 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the feature you'd like** 11 | A clear and concise description of what you want BakinBacon to do. 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine 2 | 3 | RUN apk add git make nodejs npm gcc musl-dev linux-headers 4 | 5 | WORKDIR /go/src/bakinbacon 6 | 7 | RUN git clone https://github.com/bakingbacon/bakinbacon . 8 | 9 | RUN make ui && make 10 | 11 | VOLUME /var/db 12 | 13 | EXPOSE 8082 14 | 15 | ENTRYPOINT ["/go/src/bakinbacon/bakinbacon"] 16 | -------------------------------------------------------------------------------- /webserver/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "BakinBacon", 3 | "name": "BakinBacon", 4 | "icons": [ 5 | { 6 | "src": "logo512.png", 7 | "type": "image/png", 8 | "sizes": "512x512" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /nonce/nonce_types.go: -------------------------------------------------------------------------------- 1 | package nonce 2 | 3 | var Prefix_nonce []byte = []byte{69, 220, 169} 4 | 5 | type Nonce struct { 6 | Seed string `json:"seed"` 7 | Nonce []byte `json:"noncehash"` 8 | EncodedNonce string `json:"encodednonce"` 9 | NoPrefixNonce string `json:"noprefixnonce"` 10 | 11 | Level int `json:"level"` 12 | RevealOp string `json:"revealed"` 13 | } 14 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - asciicheck 4 | - bodyclose 5 | - dogsled 6 | - goconst 7 | - gocritic 8 | - ifshort 9 | - makezero 10 | - nilerr 11 | - prealloc 12 | - tagliatelle 13 | - unparam 14 | - wastedassign 15 | disable: 16 | - nlreturn 17 | 18 | linters-settings: 19 | nlreturn: 20 | block-size: 0 21 | gocritic: 22 | disabled-checks: 23 | - ifElseChain 24 | -------------------------------------------------------------------------------- /webserver/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .eslintcache 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /docs/version.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"date":"2021-09-08T04:00:00Z", "version":"0.6.0", "notes":"Yay! Public beta release!"}, 3 | {"date":"2021-09-09T03:00:00Z", "version":"0.6.1", "notes":"- Fix for nonce reveals"}, 4 | {"date":"2021-09-21T05:00:00Z", "version":"0.7.0", "notes":"- Hangzhounet compatibility"}, 5 | {"date":"2021-10-24T02:22:00Z", "version":"0.7.1", "notes":"- More Hangzhounet fixes, Major refactor of base code"}, 6 | {"date":"2021-11-04T00:00:00Z", "version":"0.8.0", "notes":"Payouts!"}, 7 | {"date":"2021-12-14T00:00:00Z", "version":"1.1.0", "notes":"Yay! v1.1 GA!"} 8 | ] 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module bakinbacon 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Messer4/base58check v0.0.0-20180328134002-7531a92ae9ba 7 | github.com/bakingbacon/go-tezos/v4 v4.1.6 8 | github.com/bakingbacon/goledger/ledger-apps/tezos v0.0.0-20210820040404-44e1e16330dd 9 | github.com/btcsuite/btcutil v1.0.2 10 | github.com/gorilla/handlers v1.5.1 11 | github.com/gorilla/mux v1.8.0 12 | github.com/pkg/errors v0.9.1 13 | github.com/sirupsen/logrus v1.8.1 14 | go.etcd.io/bbolt v1.3.5 15 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad 16 | golang.org/x/mod v0.5.0 17 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect 18 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /storage/notifications.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | bolt "go.etcd.io/bbolt" 6 | ) 7 | 8 | func (s *Storage) GetNotifiersConfig(notifier string) ([]byte, error) { 9 | 10 | var config []byte 11 | 12 | err := s.View(func(tx *bolt.Tx) error { 13 | b := tx.Bucket([]byte(CONFIG_BUCKET)).Bucket([]byte(NOTIFICATIONS_BUCKET)) 14 | if b == nil { 15 | return errors.New("Unable to locate notifications bucket") 16 | } 17 | 18 | config = b.Get([]byte(notifier)) 19 | 20 | return nil 21 | }) 22 | 23 | return config, err 24 | } 25 | 26 | func (s *Storage) SaveNotifiersConfig(notifier string, config []byte) error { 27 | 28 | return s.Update(func(tx *bolt.Tx) error { 29 | b := tx.Bucket([]byte(CONFIG_BUCKET)).Bucket([]byte(NOTIFICATIONS_BUCKET)) 30 | if b == nil { 31 | return errors.New("Unable to locate notifications bucket") 32 | } 33 | 34 | return b.Put([]byte(notifier), config) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. We cannot read your mind! You must describe every detail to the issue. Please upload complete logs around the time the issue occurred. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /webserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webserver", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.8", 7 | "@testing-library/react": "^11.2.2", 8 | "@testing-library/user-event": "^12.6.0", 9 | "bootstrap": "^4.6.0", 10 | "react": "^17.0.1", 11 | "react-bootstrap": "^1.4.3", 12 | "react-dom": "^17.0.1", 13 | "react-icons": "^4.2.0", 14 | "react-loader-spinner": "^4.0.0", 15 | "react-number-format": "^4.4.4", 16 | "react-scripts": "^4.0.3", 17 | "web-vitals": "^0.2.4" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /webserver/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 22 | Bakin'Bacon 23 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | _ "encoding/hex" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | "golang.org/x/crypto/blake2b" 9 | ) 10 | 11 | func CryptoGenericHash(bufferBytes []byte, watermark []byte) ([]byte, error) { 12 | 13 | if len(watermark) > 0 { 14 | bufferBytes = append(watermark, bufferBytes...) 15 | } 16 | 17 | // Generic hash of 32 bytes 18 | bufferBytesHashGen, err := blake2b.New(32, []byte{}) 19 | if err != nil { 20 | return nil, errors.Wrap(err, "Unable create blake2b hash object") 21 | } 22 | 23 | // Write buffer bytes to hash 24 | if _, err = bufferBytesHashGen.Write(bufferBytes); err != nil { 25 | return nil, errors.Wrap(err, "Unable write buffer bytes to hash function") 26 | } 27 | 28 | // Generate checksum of buffer bytes 29 | bufferHash := bufferBytesHashGen.Sum([]byte{}) 30 | 31 | return bufferHash, nil 32 | } 33 | 34 | func StripQuote(s string) string { 35 | 36 | m := strings.TrimSpace(s) 37 | if len(m) > 0 && m[0] == '"' { 38 | m = m[1:] 39 | } 40 | 41 | if len(m) > 0 && m[len(m)-1] == '"' { 42 | m = m[:len(m)-1] 43 | } 44 | 45 | return m 46 | } 47 | 48 | func AvailableNetworks() string { 49 | return strings.Join([]string{NETWORK_MAINNET, NETWORK_GRANADANET, NETWORK_HANGZHOUNET}, ",") 50 | } 51 | -------------------------------------------------------------------------------- /webserver/api_voting.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/pkg/errors" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // Set delegate (from UI config) 13 | func (ws *WebServer) handleUpvote(w http.ResponseWriter, r *http.Request) { 14 | 15 | log.Debug("API - HandleUpvote") 16 | 17 | // CORS crap; Handle OPTION preflight check 18 | if r.Method == http.MethodOptions { 19 | return 20 | } 21 | 22 | k := make(map[string]interface{}) 23 | 24 | err := json.NewDecoder(r.Body).Decode(&k) 25 | if err != nil { 26 | apiError(errors.Wrap(err, "Cannot decode body for voting parameters"), w) 27 | return 28 | } 29 | 30 | proposal := k["p"].(string) 31 | period := int(k["i"].(float64)) 32 | 33 | opHash, err := ws.baconClient.UpvoteProposal(proposal, period) 34 | if err != nil { 35 | apiError(errors.Wrap(err, "Cannot cast upvote"), w) 36 | return 37 | } 38 | 39 | log.WithFields(log.Fields{ 40 | "OpHash": opHash, 41 | }).Info("Injected voting operation") 42 | 43 | // Return to UI 44 | if err := json.NewEncoder(w).Encode(map[string]string{ 45 | "ophash": opHash, 46 | }); err != nil { 47 | log.WithError(err).Error("UI Return Encode Failure") 48 | } 49 | 50 | apiReturnOk(w) 51 | } 52 | -------------------------------------------------------------------------------- /storage/watermark.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | bolt "go.etcd.io/bbolt" 5 | ) 6 | 7 | func (s *Storage) GetBakingWatermark() (int, error) { 8 | return s.getWatermark(BAKING_BUCKET) 9 | } 10 | 11 | func (s *Storage) GetEndorsingWatermark() (int, error) { 12 | return s.getWatermark(ENDORSING_BUCKET) 13 | } 14 | 15 | func (s *Storage) getWatermark(wBucket string) (int, error) { 16 | 17 | var watermark uint64 18 | 19 | err := s.View(func(tx *bolt.Tx) error { 20 | watermark = tx.Bucket([]byte(wBucket)).Sequence() 21 | return nil 22 | }) 23 | 24 | return int(watermark), err 25 | } 26 | 27 | func (s *Storage) RecordBakedBlock(level int, blockHash string) error { 28 | return s.recordOperation(BAKING_BUCKET, level, blockHash) 29 | } 30 | 31 | func (s *Storage) RecordEndorsement(level int, endorsementHash string) error { 32 | return s.recordOperation(ENDORSING_BUCKET, level, endorsementHash) 33 | } 34 | 35 | func (s *Storage) recordOperation(opBucket string, level int, opHash string) error { 36 | return s.Update(func(tx *bolt.Tx) error { 37 | b := tx.Bucket([]byte(opBucket)) 38 | if err := b.SetSequence(uint64(level)); err != nil { // Record our watermark 39 | return err 40 | } 41 | 42 | return b.Put(Itob(level), []byte(opHash)) // Save the level:opHash 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /logging.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "time" 7 | 8 | log "github.com/sirupsen/logrus" 9 | "github.com/sirupsen/logrus/hooks/writer" 10 | ) 11 | 12 | var logFile *os.File 13 | 14 | func setupLogging(logDebug bool, logTrace bool) { 15 | 16 | log.SetFormatter(&log.TextFormatter{ 17 | FullTimestamp: true, 18 | }) 19 | 20 | cwd, err := os.Getwd() 21 | if err != nil { 22 | log.Fatalf("Failed to determine working directory: %s", err) 23 | } 24 | 25 | runID := time.Now().Format("log-bakinbacon-2006-01-02") 26 | logLocation := filepath.Join(cwd, runID+".log") 27 | 28 | logFile, err = os.OpenFile(logLocation, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 29 | if err != nil { 30 | log.Fatalf("Failed to open log file %s for output: %s", logLocation, err) 31 | } 32 | 33 | if logDebug { 34 | log.SetLevel(log.DebugLevel) 35 | } 36 | 37 | if logTrace { 38 | log.SetLevel(log.TraceLevel) 39 | } 40 | 41 | // Write everything to log file too 42 | log.AddHook(&writer.Hook{ 43 | Writer: logFile, 44 | LogLevels: []log.Level{ 45 | log.TraceLevel, 46 | log.DebugLevel, 47 | log.InfoLevel, 48 | log.WarnLevel, 49 | log.ErrorLevel, 50 | log.FatalLevel, 51 | log.PanicLevel, 52 | }, 53 | }) 54 | } 55 | 56 | func closeLogging() { 57 | logFile.Close() 58 | } 59 | -------------------------------------------------------------------------------- /notifications/email.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/pkg/errors" 7 | 8 | log "github.com/sirupsen/logrus" 9 | 10 | "bakinbacon/storage" 11 | ) 12 | 13 | type NotifyEmail struct { 14 | Username string `json:"username"` 15 | Password string `json:"password"` 16 | Smtp_host string `json:"smtphost"` 17 | Smtp_port int `json:"smtpport"` 18 | Enabled bool `json:"enabled"` 19 | 20 | storage *storage.Storage 21 | } 22 | 23 | func (n *NotificationHandler) NewEmail(config []byte, saveConfig bool) (*NotifyEmail, error) { 24 | 25 | return &NotifyEmail{ 26 | Enabled: false, 27 | storage: n.storage, 28 | }, nil 29 | } 30 | 31 | func (n *NotifyEmail) IsEnabled() bool { 32 | return n.Enabled 33 | } 34 | 35 | func (n *NotifyEmail) Send(msg string) { 36 | // TODO Not implemented yet 37 | log.Warn("Email notifications not yet implemented") 38 | 39 | } 40 | 41 | func (n *NotifyEmail) SaveConfig() error { 42 | 43 | // Marshal ourselves to []byte and send to storage manager 44 | config, err := json.Marshal(n) 45 | if err != nil { 46 | return errors.Wrap(err, "Unable to marshal email config") 47 | } 48 | 49 | if err := n.storage.SaveNotifiersConfig(EMAIL, config); err != nil { 50 | return errors.Wrap(err, "Unable to save email config") 51 | } 52 | 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /storage/nonce.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/pkg/errors" 7 | 8 | bolt "go.etcd.io/bbolt" 9 | ) 10 | 11 | func (s *Storage) SaveNonce(cycle, nonceLevel int, nonceBytes []byte) error { 12 | 13 | // Nonces are stored within a cycle bucket for easy retrieval 14 | return s.Update(func(tx *bolt.Tx) error { 15 | cb, err := tx.Bucket([]byte(NONCE_BUCKET)).CreateBucketIfNotExists(Itob(cycle)) 16 | if err != nil { 17 | return errors.Wrap(err, "Unable to create nonce-cycle bucket") 18 | } 19 | 20 | return cb.Put(Itob(nonceLevel), nonceBytes) 21 | }) 22 | } 23 | 24 | func (s *Storage) GetNoncesForCycle(cycle int) ([]json.RawMessage, error) { 25 | 26 | // Get back all nonces for cycle 27 | nonces := make([]json.RawMessage, 0) 28 | 29 | err := s.Update(func(tx *bolt.Tx) error { 30 | cb, err := tx.Bucket([]byte(NONCE_BUCKET)).CreateBucketIfNotExists(Itob(cycle)) 31 | if err != nil { 32 | return errors.Wrap(err, "Unable to create nonce-cycle bucket") 33 | } 34 | c := cb.Cursor() 35 | 36 | for k, v := c.First(); k != nil; k, v = c.Next() { 37 | 38 | // Unmarshal to raw JSON 39 | var tmpRaw json.RawMessage 40 | if err := json.Unmarshal(v, &tmpRaw); err != nil { 41 | return errors.Wrap(err, "Unable to get nonces from DB") 42 | } 43 | 44 | nonces = append(nonces, tmpRaw) 45 | } 46 | 47 | return nil 48 | }) 49 | 50 | return nonces, err 51 | } 52 | -------------------------------------------------------------------------------- /webserver/src/toaster.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState, createContext } from 'react'; 2 | import Toast from 'react-bootstrap/Toast'; 3 | 4 | const ToasterContext = createContext(); 5 | 6 | export const ToasterContextProvider = ({children}) => { 7 | 8 | const [toasts, setToasts] = useState([]); 9 | 10 | const addToast = useCallback((toast) => { 11 | toast.id = Math.floor((Math.random() * 10001) + 1); 12 | toast.autohide = toast.autohide || 0; 13 | 14 | // Don't display more than 10 messages 15 | setToasts((t) => { 16 | if (t.length > 9) { 17 | t.shift(); 18 | } 19 | return [...t, toast]; 20 | }); 21 | }, 22 | [] 23 | ); 24 | 25 | const deleteToast = (id) => { 26 | setToasts((t) => t.filter(e => e.id !== id)); 27 | }; 28 | 29 | return ( 30 | 31 | {children} 32 |
33 | { toasts.map((toast) => ( 34 | deleteToast(toast.id)} 37 | className={"toaster-"+toast.type} 38 | {... (toast.autohide > 0 ? {delay: toast.autohide, autohide: true} : {})} 39 | > 40 | 41 | {toast.title} 42 | 43 | {toast.msg} 44 | 45 | )) 46 | } 47 |
48 |
49 | ); 50 | } 51 | 52 | export default ToasterContext; 53 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOCMD=go 3 | GOBUILD=$(GOCMD) build -v 4 | GOCLEAN=$(GOCMD) clean 5 | GOFMT=gofmt -d -s 6 | GOGET=$(GOCMD) get 7 | BINARY_NAME=bakinbacon 8 | 9 | LINUX_BINARY=$(BINARY_NAME)-linux-amd64 10 | DARWIN_BINARY=$(BINARY_NAME)-darwin-amd64 11 | WINDOWS_BASE=$(BINARY_NAME)-windows-amd64 12 | WINDOWS_BINARY=$(WINDOWS_BASE).exe 13 | 14 | GIT_COMMIT := $(shell git rev-list -1 HEAD | cut -c 1-6) 15 | SOURCES := $(shell find ./ -name '*.go') 16 | PWD=$(shell pwd) 17 | 18 | all: build 19 | 20 | build: $(SOURCES) 21 | $(GOBUILD) -o $(LINUX_BINARY) -ldflags "-X main.commitHash=$(GIT_COMMIT)" 22 | 23 | dist: $(LINUX_BINARY) 24 | tar -cvzf $(LINUX_BINARY).tar.gz $(LINUX_BINARY) 25 | 26 | darwin: $(SOURCES) 27 | $(GOBUILD) -o $(DARWIN_BINARY) -ldflags "-X main.commitHash=$(GIT_COMMIT)" 28 | 29 | darwin-dist: $(DARWIN_BINARY) 30 | tar -cvzf $(DARWIN_BINARY).tar.gz $(DARWIN_BINARY) 31 | 32 | windows: $(SOURCES) 33 | docker run --rm -v golang-windows-cache:/go/pkg -v $(PWD):/go/src/bakinbacon -w /go/src/bakinbacon -e GOCACHE=/go/pkg/.cache x1unix/go-mingw /bin/sh -c "go build -v -o $(WINDOWS_BINARY) -ldflags '-X main.commitHash=$(GIT_COMMIT)'" 34 | 35 | windows-dist: $(WINDOWS_BINARY) 36 | tar -cvzf $(WINDOWS_BASE).tar.gz $(WINDOWS_BINARY) 37 | 38 | fmt: 39 | $(GOFMT) baconclient/ nonce/ notifications/ storage/ util/ webserver/ *.go 40 | 41 | clean: 42 | rm -f *.tar.gz $(LINUX_BINARY) $(DARWIN_BINARY) $(WINDOWS_BINARY) 43 | 44 | ui-dev: 45 | npm --prefix webserver/ install 46 | 47 | ui: 48 | npm --prefix webserver/ run build 49 | -------------------------------------------------------------------------------- /webserver/api.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | 11 | log "github.com/sirupsen/logrus" 12 | 13 | "bakinbacon/baconclient" 14 | ) 15 | 16 | // Dummy health check 17 | func (ws *WebServer) getHealth(w http.ResponseWriter, r *http.Request) { 18 | 19 | log.Debug("API - GetHealth") 20 | 21 | if err := json.NewEncoder(w).Encode(map[string]bool{ 22 | "ok": true, 23 | }); err != nil { 24 | log.WithError(err).Error("Heath Check Failure") 25 | } 26 | } 27 | 28 | // Get current status 29 | func (ws *WebServer) getStatus(w http.ResponseWriter, r *http.Request) { 30 | 31 | log.Debug("API - GetStatus") 32 | 33 | _, pkh, err := ws.storage.GetDelegate() 34 | if err != nil { 35 | apiError(errors.Wrap(err, "Cannot get delegate"), w) 36 | return 37 | } 38 | 39 | s := struct { 40 | *baconclient.BaconStatus 41 | Delegate string `json:"pkh"` 42 | Timestamp int64 `json:"ts"` 43 | }{ 44 | ws.baconClient.Status, 45 | pkh, 46 | time.Now().Unix(), 47 | } 48 | 49 | if err := json.NewEncoder(w).Encode(s); err != nil { 50 | log.WithError(err).Error("UI Return Encode Failure") 51 | } 52 | } 53 | 54 | // 55 | // Set delegate (from UI config) 56 | func (ws *WebServer) setDelegate(w http.ResponseWriter, r *http.Request) { 57 | 58 | body, err := ioutil.ReadAll(r.Body) 59 | if err != nil { 60 | apiError(errors.Wrap(err, "Cannot read set delegate"), w) 61 | return 62 | } 63 | 64 | pkh := string(body) 65 | 66 | // No esdk if using ledger 67 | if err := ws.storage.SetDelegate("", pkh); err != nil { 68 | apiError(errors.Wrap(err, "Cannot set delegate"), w) 69 | return 70 | } 71 | 72 | log.WithField("PKH", pkh).Debug("API - SetDelegate") 73 | 74 | apiReturnOk(w) 75 | } 76 | -------------------------------------------------------------------------------- /baconclient/baconstatus.go: -------------------------------------------------------------------------------- 1 | package baconclient 2 | 3 | const ( 4 | 5 | // Various states for the UI to take action 6 | CAN_BAKE = "canbake" 7 | LOW_BALANCE = "lowbal" 8 | NOT_REGISTERED = "noreg" 9 | NO_SIGNER = "nosign" 10 | ) 11 | 12 | type BaconStatus struct { 13 | Network string `json:"net"` 14 | Hash string `json:"hash"` 15 | Level int `json:"level"` 16 | Cycle int `json:"cycle"` 17 | CyclePosition int `json:"cycleposition"` 18 | 19 | NextEndorsementLevel int `json:"nel"` 20 | NextEndorsementCycle int `json:"nec"` 21 | 22 | NextBakingLevel int `json:"nbl"` 23 | NextBakingCycle int `json:"nbc"` 24 | NextBakingPriority int `json:"nbp"` 25 | 26 | PreviousEndorsementLevel int `json:"pel"` 27 | PreviousEndorsementCycle int `json:"pec"` 28 | PreviousEndorsementHash string `json:"peh"` 29 | 30 | PreviousBakeLevel int `json:"pbl"` 31 | PreviousBakeCycle int `json:"pbc"` 32 | PreviousBakeHash string `json:"pbh"` 33 | 34 | State string `json:"state"` 35 | ErrorMsg string `json:"error"` 36 | } 37 | 38 | func (b *BaconStatus) SetNextEndorsement(level, cycle int) { 39 | b.NextEndorsementLevel = level 40 | b.NextEndorsementCycle = cycle 41 | } 42 | 43 | func (b *BaconStatus) SetNextBake(level, cycle, priority int) { 44 | b.NextBakingLevel = level 45 | b.NextBakingCycle = cycle 46 | b.NextBakingPriority = priority 47 | } 48 | 49 | func (b *BaconStatus) SetRecentEndorsement(level, cycle int, hash string) { 50 | b.PreviousEndorsementLevel = level 51 | b.PreviousEndorsementCycle = cycle 52 | b.PreviousEndorsementHash = hash 53 | } 54 | 55 | func (b *BaconStatus) SetRecentBake(level, cycle int, hash string) { 56 | b.PreviousBakeLevel = level 57 | b.PreviousBakeCycle = cycle 58 | b.PreviousBakeHash = hash 59 | } 60 | 61 | func (b *BaconStatus) SetError(e error) { 62 | b.ErrorMsg = e.Error() 63 | } 64 | 65 | func (b *BaconStatus) ClearError() { 66 | b.ErrorMsg = "" 67 | } 68 | 69 | func (b *BaconStatus) SetState(s string) { 70 | b.State = s 71 | } 72 | -------------------------------------------------------------------------------- /util/networkconstants.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | NETWORK_MAINNET = "mainnet" 9 | NETWORK_GRANADANET = "granadanet" 10 | NETWORK_HANGZHOUNET = "hangzhounet" 11 | ) 12 | 13 | type NetworkConstants struct { 14 | TimeBetweenBlocks int 15 | BlocksPerCycle int 16 | BlocksPerRollSnapshot int 17 | BlocksPerCommitment int 18 | BlockGasLimit int 19 | BlockSecurityDeposit int 20 | EndorsementSecurityDeposit int 21 | ProofOfWorkThreshold uint64 22 | PreservedCycles int 23 | InitialEndorsers int 24 | GranadaActivationLevel int 25 | GranadaActivationCycle int 26 | // Granada changed the simple calculations, so we need to 27 | // know the last level before the change. For mainnet, 28 | // this happened just before C388 (388 * 4096 - 1) 29 | } 30 | 31 | // For updating, mainnet example 32 | // curl -Ss https://mainnet-tezos.giganode.io/chains/main/blocks/head/context/constants | jq -r '[ (.minimal_block_delay|tonumber), .blocks_per_cycle, .blocks_per_roll_snapshot, .blocks_per_commitment, (.hard_gas_limit_per_block|tonumber), (.block_security_deposit|tonumber), (.endorsement_security_deposit|tonumber), (.proof_of_work_threshold|tonumber), .preserved_cycles, .initial_endorsers] | @csv' 33 | 34 | func GetNetworkConstants(network string) (*NetworkConstants, error) { 35 | 36 | switch network { 37 | case NETWORK_MAINNET: 38 | return &NetworkConstants{ 39 | 30, 8192, 512, 64, 5200000, 64000000, 2500000, 70368744177663, 5, 192, 1589247, 388, 40 | }, nil 41 | case NETWORK_GRANADANET: 42 | return &NetworkConstants{ 43 | 15, 4096, 256, 32, 5200000, 640000000, 2500000, 70368744177663, 3, 192, 4095, 2, 44 | }, nil 45 | case NETWORK_HANGZHOUNET: 46 | return &NetworkConstants{ 47 | 15, 4096, 256, 32, 5200000, 640000000, 2500000, 70368744177663, 3, 192, 0, 0, 48 | }, nil 49 | } 50 | 51 | // Unknown network 52 | return nil, fmt.Errorf("No such network '%s' exists", network) 53 | } 54 | 55 | func IsValidNetwork(maybeNetwork string) bool { 56 | return maybeNetwork == NETWORK_MAINNET || maybeNetwork == NETWORK_GRANADANET || maybeNetwork == NETWORK_HANGZHOUNET 57 | } 58 | -------------------------------------------------------------------------------- /webserver/src/settings/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react'; 2 | 3 | import Col from 'react-bootstrap/Col'; 4 | import Row from 'react-bootstrap/Row'; 5 | 6 | import Notifications from './notifications.js' 7 | import Rpcservers from './rpcservers.js' 8 | import BakerSettings from './bakersettings.js' 9 | 10 | import ToasterContext from '../toaster.js'; 11 | import { apiRequest } from '../util.js'; 12 | 13 | const UI_EXPLORER = "uiexplorer"; // bakinbacon/storage/config.go 14 | 15 | const Settings = (props) => { 16 | 17 | const [ settings, updateSettings ] = useState({endpoints:{},notifications:{}}) 18 | const [ isLoading, setIsLoading ] = useState(true); 19 | const addToast = useContext(ToasterContext); 20 | 21 | useEffect(() => { 22 | loadSettings(); 23 | // eslint-disable-next-line react-hooks/exhaustive-deps 24 | }, []); 25 | 26 | const loadSettings = () => { 27 | const apiUrl = window.BASE_URL + "/api/settings/"; 28 | apiRequest(apiUrl) 29 | .then((data) => { 30 | updateSettings((prev) => ({ ...prev, ...data })) 31 | }) 32 | .catch((errMsg) => { 33 | console.log(errMsg); 34 | addToast({ 35 | title: "Loading Settings Error", 36 | msg: errMsg, 37 | type: "danger", 38 | }); 39 | }) 40 | .finally(() => { 41 | setIsLoading(false); 42 | }) 43 | }; 44 | 45 | if (isLoading) { 46 | return ( 47 |

Loading...

48 | ) 49 | } 50 | 51 | return ( 52 | <> 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ) 68 | } 69 | 70 | // Several locations need the UI source 71 | export const GetUiExplorer = (setExplorer) => { 72 | const apiUrl = window.BASE_URL + "/api/settings/"; 73 | apiRequest(apiUrl) 74 | .then((data) => { 75 | setExplorer(data["baker"][UI_EXPLORER]); 76 | }) 77 | .catch((errMsg) => { 78 | console.log(errMsg) 79 | setExplorer("tzstats.com") 80 | }) 81 | } 82 | 83 | export default Settings -------------------------------------------------------------------------------- /webserver/src/nextopportunities.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Col from 'react-bootstrap/Col'; 4 | import Card from 'react-bootstrap/Card'; 5 | import Row from 'react-bootstrap/Row'; 6 | 7 | const NextOpportunities = (props) => { 8 | 9 | const status = props.status 10 | 11 | const nextBake = () => { 12 | const nextBakeSeconds = (status.nbl - status.level) * window.MIN_BLOCK_TIME 13 | const t = new Date() 14 | t.setSeconds(t.getSeconds() + nextBakeSeconds) 15 | return ( 16 | <> 17 | Baking 18 | Level: {status.nbl} 19 | Cycle: {status.nbc} 20 | Priority: {status.nbp} 21 | Est. Time: {t.toLocaleString()} 22 | 23 | ) 24 | } 25 | 26 | const noBaking = () => { 27 | return ( 28 | <> 29 | Baking 30 | No baking rights found for this cycle. 31 | No baking rights found for next cycle. 32 | 33 | ) 34 | } 35 | 36 | const nextEndorsement = () => { 37 | return ( 38 | <> 39 | Endorsement 40 | Level: {status.nel} 41 | Cycle: {status.nec} 42 | 43 | ) 44 | } 45 | 46 | const noEndorsements = () => { 47 | return ( 48 | <> 49 | Endorsement 50 | No endorsements found for this cycle. 51 | No endorsements found for next cycle. 52 | 53 | ) 54 | } 55 | 56 | return ( 57 | 58 | Next Opportunity 59 | 60 | 61 | 62 | { status.nbl === 0 && noBaking() } 63 | { status.nbl > 0 && nextBake() } 64 | 65 | 66 | { status.nel === 0 && noEndorsements() } 67 | { status.nel > 0 && nextEndorsement() } 68 | 69 | 70 | 71 | 72 | ) 73 | } 74 | 75 | export default NextOpportunities; 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | --- 4 | 5 | ## Running BakinBacon 6 | 7 | _BakinBacon defaults to Hangzhounet, the current Tezos testing network. Use `-network mainnet` to switch._ 8 | 9 | 1. Download the latest binary for your OS from [bakinbacon/releases](https://github.com/bakingbacon/bakinbacon/releases) 10 | 1. Open a terminal, shell, cmd, powershell, etc and execute the binary for your operating system: 11 | 12 | Example: `./bakinbacon-linux-amd64 [-debug] [-trace] [-webuiaddr 127.0.0.1] [-webuiport 8082] [-network mainnet|granadanet|hangzhounet]` 13 | 14 | 3. Open http://127.0.0.1:8082/ in your browser 15 | 16 | The following binaries are available as part of our release process: 17 | 18 | * bakinbacon-linux-amd64 19 | * bakinbacon-darwin-amd64 20 | * bakinbacon-windows-amd64.exe 21 | 22 | If you would like bakinbacon compiled for a different platform, you can build it yourself below, or open an issue and we might be able to add it to our build prcocess. 23 | 24 | ### Testing Tokens 25 | 26 | The Tezos network requires 8000 XTZ at stake in order to be considered a baker. Please use the [hangzhou faucet](https://faucet.hangzhounet.teztnets.xyz/) to acquire testing tokens. These tokens are only valid on the Hangzhou testing network and will not work on mainnet. 27 | 28 | ## Building BakinBacon 29 | 30 | If you want to contribute to BakinBacon with pull-requests, you'll need a proper environment set up to build and test BakinBacon. 31 | 32 | ### Dependencies 33 | 34 | * go-1.16+ 35 | * nodejs-14.15 (npm 6.14) 36 | * gcc-7.5+, make (build-essential package on Ubuntu) 37 | * libhidapi-libusb0, libusb, libusb-dev (For compiling ledger nano support) 38 | 39 | ### Ledger Usage 40 | 41 | If you want to use a Ledger device with BakinBacon, you will need to [download and install](https://www.ledger.com/ledger-live/download) Ledger Live, and install **BOTH** Tezos Wallet and Tezos Baker apps to your device. We **DO NOT** recommend any version higher than 2.2.9 as they are buggy and prone to device freeze. 42 | * If using a ledger on linux, you'll need to add the [udev rules](https://support.ledger.com/hc/en-us/articles/115005165269-Fix-USB-connection-issues-with-Ledger-Live). 43 | 44 | ### Build Steps 45 | 46 | 1. Clone the repo 47 | 1. `make ui-dev && make ui` (Build the webserver UI, downloading any required npm modules) 48 | 1. `make [darwin|windows]` (You can only build darwin on darwin; You can build linux and windows on linux) 49 | 1. Run as noted above 50 | -------------------------------------------------------------------------------- /webserver/src/util.js: -------------------------------------------------------------------------------- 1 | // Utility functions and constants for BakinBacon 2 | 3 | import Alert from 'react-bootstrap/Alert' 4 | import Col from 'react-bootstrap/Col'; 5 | import Row from 'react-bootstrap/Row'; 6 | 7 | import { FaCheckCircle, FaExclamationTriangle } from "react-icons/fa"; 8 | 9 | export const LOW_BALANCE = "lowbal" 10 | export const NO_SIGNER = "nosign" 11 | export const CAN_BAKE = "canbake" 12 | export const NOT_REGISTERED = "noreg" 13 | 14 | export const CHAINIDS = { 15 | "mainnet": "NetXdQprcVkpaWU", 16 | "florencenet": "NetXxkAx4woPLyu", 17 | "granadanet": "NetXz969SFaFn8k", 18 | "hangzhounet": "NetXuXoGoLxNK6o", 19 | }; 20 | 21 | // Copied from https://github.com/github/fetch/issues/203#issuecomment-266034180 22 | function parseJSON(response) { 23 | 24 | // No JSON to parse, return custom object 25 | if (response.status !== 200 && response.status !== 400 && response.status !== 502) { 26 | return new Promise((resolve) => resolve({ 27 | status: response.status, 28 | ok: response.ok, 29 | json: {"error": "Error Fetching URL"} 30 | })); 31 | } 32 | 33 | // Handles 200 OK and custom 400 JSON-encoded errors from API 34 | return new Promise((resolve) => response.json() 35 | .then((json) => resolve({ 36 | status: response.status, 37 | ok: response.ok, 38 | json, 39 | })) 40 | ); 41 | } 42 | 43 | export function apiRequest(url, options) { 44 | return new Promise((resolve, reject) => { 45 | fetch(url, options) 46 | .then(parseJSON) 47 | .then((response) => { 48 | // If 200 OK, resolve with JSON 49 | if (response.ok) { 50 | return resolve(response.json); 51 | } 52 | // Extract custom error message 53 | return reject(response.json.error); 54 | }) 55 | .catch((error) => reject(error.message)); // Network errors 56 | }); 57 | } 58 | 59 | export const BaconAlert = (props) => { 60 | 61 | const { alert } = props; 62 | 63 | if (!alert.msg) { 64 | return null; 65 | } 66 | 67 | return ( 68 | 69 | 70 | 71 | { alert.type === "success" && 72 | 73 | } 74 | { alert.type === "danger" && 75 | 76 | } 77 | {alert.msg} 78 | { alert.debug && 79 | <> 80 |
Error: {alert.debug} 81 | 82 | } 83 |
84 | 85 |
86 | ) 87 | } 88 | 89 | export const substr = (g) => { 90 | return String(g).substring(0, 10) 91 | } 92 | 93 | export const muToTez = (x) => { 94 | const y = parseInt(x) 95 | return y/1e6 96 | } 97 | -------------------------------------------------------------------------------- /webserver/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 14px "Century Gothic", Futura, sans-serif; 3 | margin: 20px; 4 | } 5 | 6 | a.nav-link { 7 | color: #495057; 8 | } 9 | 10 | .bg-light { 11 | background-color: #eeeeee; 12 | } 13 | 14 | .card-header { 15 | background-color: #eeeeee; 16 | } 17 | 18 | div.row { 19 | margin-top: 1em !important; 20 | } 21 | 22 | .baconstatus { 23 | display: inline-block; 24 | width: 25px; 25 | height: 25px; 26 | vertical-align: text-bottom; 27 | border-radius: 50%; 28 | margin-right: 10px; 29 | } 30 | 31 | .baconstatus-red { 32 | background-color: red; 33 | } 34 | 35 | .baconstatus-green { 36 | background-color: green; 37 | } 38 | 39 | .table-balances td { text-align: left; } 40 | .table-balances td:first-child { 41 | text-align: right; 42 | width: 200px; 43 | } 44 | 45 | .stats-title, .stats-title-w { 46 | display: inline-block; 47 | text-align: right; 48 | width: 100px; 49 | } 50 | .stats-title-w { 51 | width: 150px; 52 | } 53 | 54 | .stats-val { 55 | display: inline-block; 56 | text-align: left; 57 | } 58 | 59 | .btn-primary { 60 | background-color: #ED9051; 61 | border-color: #D65337; 62 | } 63 | 64 | .padded-top-30 { 65 | padding-top: 30px; 66 | } 67 | 68 | .tab-pane { 69 | border-left: 1px solid #ddd; 70 | border-right: 1px solid #ddd; 71 | border-bottom: 1px solid #ddd; 72 | border-radius: 0px 0px 5px 5px; 73 | padding: 10px; 74 | } 75 | 76 | .nav-tabs { 77 | margin-bottom: 0; 78 | } 79 | 80 | .navbar { 81 | margin-bottom: 5px; 82 | border: 1px solid #ddd; 83 | border-radius: 5px; 84 | } 85 | 86 | .toasters-container { 87 | position: fixed; 88 | z-index: 999999; 89 | top: 12px; 90 | right: 12px; 91 | } 92 | 93 | /* Main toaster box */ 94 | .toast { 95 | width: 300px; 96 | } 97 | 98 | .toaster-warning, .toaster-warning .toast-header { 99 | color: #eee; 100 | background-color: #f0ad4e; 101 | } 102 | 103 | .toaster-danger, .toaster-danger .toast-header { 104 | color: #eee; 105 | background-color: #d9534f; 106 | } 107 | 108 | .toaster-success, .toaster-success .toast-header { 109 | color: #eee; 110 | background-color: #5cb85c; 111 | } 112 | 113 | .toaster-primary, .toaster-primary .toast-header { 114 | color: #004085; 115 | background-color: #cce5ff; 116 | } 117 | 118 | .toaster-info, .toaster-info .toast-header { 119 | color: #0c5460; 120 | background-color: #bee5eb; 121 | } 122 | 123 | .alert-secondary { 124 | background-color: #f8f9fa; 125 | } 126 | 127 | svg.ledgerAlert { 128 | margin-bottom: 4px; 129 | margin-right: 5px; 130 | } 131 | 132 | .row-bottom-border { 133 | border-bottom: 1px solid black; 134 | } 135 | 136 | .row-top-border td { 137 | border-top: 2px solid black; 138 | } 139 | 140 | -------------------------------------------------------------------------------- /webserver/src/dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | import Card from 'react-bootstrap/Card'; 4 | import Col from 'react-bootstrap/Col'; 5 | import ProgressBar from 'react-bootstrap/ProgressBar'; 6 | import Row from 'react-bootstrap/Row'; 7 | 8 | import DelegateInfo from './delegateinfo.js' 9 | import NextOpportunities from './nextopportunities.js' 10 | import { BaconAlert, CAN_BAKE, NO_SIGNER, substr } from './util.js' 11 | 12 | const BaconDashboard = (props) => { 13 | 14 | const { uiExplorer, delegate, status } = props; 15 | const [ alert, setAlert ] = useState({}) 16 | 17 | useEffect(() => { 18 | 19 | if (status.state === NO_SIGNER) { 20 | setAlert({ 21 | type: "danger", 22 | msg: "No signer is configured. If using a ledger, is it plugged in? Unlocked? Baking app open?", 23 | debug: status.error, 24 | }); 25 | } 26 | 27 | return null; 28 | 29 | // eslint-disable-next-line react-hooks/exhaustive-deps 30 | }, []); 31 | 32 | return ( 33 | <> 34 | 35 | 36 | 37 | Current Status 38 | 39 | Level: {status.level} 40 | Cycle: {status.cycle} 41 | Hash: {substr(status.hash)} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | { status.state === NO_SIGNER && 50 | 51 | } 52 | 53 | { status.state === CAN_BAKE && 54 | 55 | 56 | 57 | Recent Activity 58 | 59 | 60 | 61 | Baking 62 | Level: {status.pbl} 63 | Cycle: {status.pbc} 64 | Hash: {substr(status.pbh)} 65 | 66 | 67 | Endorsement 68 | Level: {status.pel} 69 | Cycle: {status.pec} 70 | Hash: {substr(status.peh)} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | } 81 | 82 | ) 83 | } 84 | 85 | export default BaconDashboard 86 | -------------------------------------------------------------------------------- /versioncheck.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "runtime" 10 | "time" 11 | 12 | "github.com/pkg/errors" 13 | "golang.org/x/mod/semver" 14 | 15 | log "github.com/sirupsen/logrus" 16 | 17 | "bakinbacon/notifications" 18 | ) 19 | 20 | const ( 21 | VERSION_URL = "https://bakingbacon.github.io/bakinbacon/version.json" 22 | STATS_URL = "https://bakinbacon.io/stats.php" 23 | ) 24 | 25 | var ( 26 | commitHash string 27 | version = "v1.1.0" 28 | ) 29 | 30 | type Versions []Version 31 | 32 | type Version struct { 33 | Date time.Time `json:"date"` 34 | Version string `json:"version"` 35 | Notes string `json:"notes"` 36 | } 37 | 38 | func (bb *BakinBacon) RunVersionCheck() { 39 | 40 | // Check every 8hrs 41 | ticker := time.NewTicker(8 * time.Hour) 42 | 43 | for { 44 | 45 | bb.submitAnonymousStats() 46 | 47 | versions := Versions{} 48 | 49 | log.Info("Checking version...") 50 | 51 | // HTTP client 10s timeout 52 | client := &http.Client{ 53 | Timeout: time.Second * 10, 54 | } 55 | 56 | // Anon func to get defer ability 57 | err := func() error { 58 | resp, err := client.Get(VERSION_URL) 59 | if err != nil { 60 | return errors.Wrap(err, "Unable to get version update") 61 | } 62 | defer resp.Body.Close() 63 | 64 | if err := json.NewDecoder(resp.Body).Decode(&versions); err != nil { 65 | return errors.Wrap(err, "Unable to decode version check") 66 | } 67 | return nil 68 | }() 69 | if err != nil { 70 | log.WithError(err).Error("Error checking version") 71 | } else { 72 | 73 | // Assume JSON is in version order, get the latest entry 74 | latestVersion := versions[0] 75 | 76 | // If newer version available, send notification 77 | if semver.Compare(version, latestVersion.Version) == -1 { 78 | bb.NotificationHandler.SendNotification(fmt.Sprintf("A new version, %s, of Bakin'Bacon is available! You are currently running %s.", 79 | latestVersion, version), notifications.VERSION) 80 | } 81 | } 82 | 83 | // wait here for next iteration 84 | <-ticker.C 85 | } 86 | } 87 | 88 | func (bb *BakinBacon) submitAnonymousStats() { 89 | 90 | // Get PKH of baker 91 | _, pkh, err := bb.Signer.GetPublicKey() 92 | if err != nil { 93 | log.WithError(err).Error("Unable to get PKH for stats") 94 | return 95 | } 96 | 97 | // create hash of PKH to anonymize the info 98 | uuid := fmt.Sprintf("%x", md5.Sum([]byte(pkh))) 99 | 100 | // json of stats 101 | stats, _ := json.Marshal(map[string]string{ 102 | "uuid": uuid, 103 | "os": fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH), 104 | "chash": commitHash, 105 | "bbver": version, 106 | }) 107 | 108 | statsPostBody := bytes.NewBuffer(stats) 109 | resp, err := http.Post(STATS_URL, "application/json", statsPostBody) 110 | if err != nil { 111 | log.WithError(err).Error("Unable to post stats") 112 | return 113 | } 114 | defer resp.Body.Close() 115 | 116 | log.Debug("Posted anonymous stats") 117 | } 118 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: bacon-builder 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | env: 7 | WEBSERVER_PATH: ${{ github.workspace }}/webserver 8 | 9 | jobs: 10 | macos-build: 11 | runs-on: macos-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: '1.16' 20 | 21 | - name: Setup Node/NPM 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: 14.15 25 | 26 | - name: Cache node_modules 27 | uses: actions/cache@v2 28 | with: 29 | key: bakinbacon-npm-${{ hashFiles('**/package-lock.json') }} 30 | path: "${{ env.WEBSERVER_PATH }}/node_modules" 31 | 32 | - name: Install node_modules 33 | working-directory: ${{ env.WEBSERVER_PATH }} 34 | run: npm install 35 | 36 | - name: Build web ui 37 | working-directory: ${{ env.WEBSERVER_PATH }} 38 | env: 39 | CI: "false" 40 | run: npm run build 41 | 42 | - name: Bellybutton lint 43 | uses: golangci/golangci-lint-action@v2 44 | with: 45 | working-directory: ${{ github.workspace }} 46 | version: latest 47 | skip-go-installation: true 48 | args: --timeout 300s 49 | 50 | - name: Fry some darwin bacon 51 | run: make darwin && make darwin-dist 52 | 53 | - name: Release 54 | uses: softprops/action-gh-release@v1 55 | if: startsWith(github.ref, 'refs/tags/') 56 | with: 57 | files: bakinbacon-darwin-amd64.tar.gz 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | 61 | linux-windows-build: 62 | runs-on: ubuntu-latest 63 | steps: 64 | 65 | - name: Checkout code 66 | uses: actions/checkout@v2 67 | 68 | - name: Setup Go 69 | uses: actions/setup-go@v2 70 | with: 71 | go-version: '1.16' 72 | 73 | - name: Setup Node/NPM 74 | uses: actions/setup-node@v1 75 | with: 76 | node-version: 14.15 77 | 78 | - name: Cache node_modules 79 | uses: actions/cache@v2 80 | with: 81 | key: bakinbacon-npm-${{ hashFiles('**/package-lock.json') }} 82 | path: "${{ env.WEBSERVER_PATH }}/node_modules" 83 | 84 | - name: Install node_modules 85 | working-directory: ${{ env.WEBSERVER_PATH }} 86 | run: npm install 87 | 88 | - name: Build web ui 89 | working-directory: ${{ env.WEBSERVER_PATH }} 90 | env: 91 | CI: "false" 92 | run: npm run build 93 | 94 | - name: Bellybutton lint 95 | uses: golangci/golangci-lint-action@v2 96 | with: 97 | version: latest 98 | skip-go-installation: true 99 | 100 | - name: Fry some linux bacon 101 | run: make && make dist 102 | 103 | - name: Fry some windows bacon 104 | run: make windows && make windows-dist 105 | 106 | - name: Release 107 | uses: softprops/action-gh-release@v1 108 | if: startsWith(github.ref, 'refs/tags/') 109 | with: 110 | files: | 111 | bakinbacon-linux-amd64.tar.gz 112 | bakinbacon-windows-amd64.tar.gz 113 | env: 114 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 115 | -------------------------------------------------------------------------------- /storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/binary" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | 9 | log "github.com/sirupsen/logrus" 10 | bolt "go.etcd.io/bbolt" 11 | ) 12 | 13 | const ( 14 | DATABASE_FILE = "bakinbacon.db" 15 | 16 | BAKING_BUCKET = "bakes" 17 | ENDORSING_BUCKET = "endorses" 18 | NONCE_BUCKET = "nonces" 19 | CONFIG_BUCKET = "config" 20 | RIGHTS_BUCKET = "rights" 21 | ENDPOINTS_BUCKET = "endpoints" 22 | NOTIFICATIONS_BUCKET = "notifs" 23 | PAYOUTS_BUCKET = "payouts" 24 | ) 25 | 26 | type Storage struct { 27 | *bolt.DB 28 | } 29 | 30 | func InitStorage(dataDir, network string) (*Storage, error) { 31 | 32 | db, err := bolt.Open(dataDir+DATABASE_FILE, 0600, &bolt.Options{Timeout: 1 * time.Second}) 33 | if err != nil { 34 | return nil, errors.Wrap(err, "Failed to init db") 35 | } 36 | 37 | // Ensure some buckets exist, and migrations 38 | err = db.Update(func(tx *bolt.Tx) error { 39 | 40 | // Config bucket 41 | cfgBkt, err := tx.CreateBucketIfNotExists([]byte(CONFIG_BUCKET)) 42 | if err != nil { 43 | return errors.Wrap(err, "Cannot create config bucket") 44 | } 45 | 46 | // Nested bucket inside config 47 | if _, err := cfgBkt.CreateBucketIfNotExists([]byte(ENDPOINTS_BUCKET)); err != nil { 48 | return errors.Wrap(err, "Cannot create endpoints bucket") 49 | } 50 | 51 | // Nested bucket inside config 52 | if _, err := cfgBkt.CreateBucketIfNotExists([]byte(NOTIFICATIONS_BUCKET)); err != nil { 53 | return errors.Wrap(err, "Cannot create notifications bucket") 54 | } 55 | 56 | // 57 | // Root buckets 58 | if _, err := tx.CreateBucketIfNotExists([]byte(ENDORSING_BUCKET)); err != nil { 59 | return errors.Wrap(err, "Cannot create endorsing bucket") 60 | } 61 | 62 | if _, err := tx.CreateBucketIfNotExists([]byte(BAKING_BUCKET)); err != nil { 63 | return errors.Wrap(err, "Cannot create baking bucket") 64 | } 65 | 66 | if _, err := tx.CreateBucketIfNotExists([]byte(NONCE_BUCKET)); err != nil { 67 | return errors.Wrap(err, "Cannot create nonce bucket") 68 | } 69 | 70 | if _, err := tx.CreateBucketIfNotExists([]byte(RIGHTS_BUCKET)); err != nil { 71 | return errors.Wrap(err, "Cannot create rights bucket") 72 | } 73 | 74 | if _, err := tx.CreateBucketIfNotExists([]byte(PAYOUTS_BUCKET)); err != nil { 75 | return errors.Wrap(err, "Cannot create payouts bucket") 76 | } 77 | 78 | return nil 79 | }) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | // set variable so main program can access 85 | storage := &Storage{ 86 | DB: db, 87 | } 88 | 89 | // Add the default endpoints only on brand new setup 90 | if err := storage.AddDefaultEndpoints(network); err != nil { 91 | log.WithError(err).Error("Could not add default endpoints") 92 | return nil, errors.Wrap(err, "Could not add default endpoints") 93 | } 94 | 95 | return storage, err 96 | } 97 | 98 | func (s *Storage) CloseDb() { 99 | s.Close() 100 | log.Info("Database closed") 101 | } 102 | 103 | // Itob returns an 8-byte big endian representation of v. 104 | func Itob(v int) []byte { 105 | b := make([]byte, 8) 106 | binary.BigEndian.PutUint64(b, uint64(v)) 107 | return b 108 | } 109 | 110 | func Btoi(b []byte) int { 111 | if b != nil { 112 | return int(binary.BigEndian.Uint64(b)) 113 | } 114 | return 0 115 | } 116 | -------------------------------------------------------------------------------- /baconsigner/wallet.go: -------------------------------------------------------------------------------- 1 | package baconsigner 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | 6 | gtks "github.com/bakingbacon/go-tezos/v4/keys" 7 | log "github.com/sirupsen/logrus" 8 | 9 | "bakinbacon/storage" 10 | ) 11 | 12 | type WalletSigner struct { 13 | sk string 14 | pkh string 15 | wallet *gtks.Key 16 | storage *storage.Storage 17 | } 18 | 19 | var W *WalletSigner 20 | 21 | func InitWalletSigner(db *storage.Storage) error { 22 | 23 | W = &WalletSigner{ 24 | storage: db, 25 | } 26 | 27 | walletSk, err := W.storage.GetSignerSk() 28 | if err != nil { 29 | return errors.Wrap(err, "Unable to get signer sk from DB") 30 | } 31 | 32 | if walletSk == "" { 33 | return errors.New("No wallet secret key found. Cannot bake.") 34 | } 35 | 36 | // Import key 37 | wallet, err := gtks.FromBase58(walletSk, gtks.Ed25519) 38 | if err != nil { 39 | return errors.Wrap(err, "Failed to load wallet from secret key") 40 | } 41 | 42 | W.wallet = wallet 43 | W.pkh = wallet.PubKey.GetAddress() 44 | 45 | log.WithFields(log.Fields{ 46 | "Baker": W.pkh, "PublicKey": W.wallet.PubKey.GetPublicKey(), 47 | }).Info("Loaded software wallet") 48 | 49 | return nil 50 | } 51 | 52 | // GenerateNewKey Generates a new ED25519 keypair; Only used on first setup through UI wizard so init the signer here 53 | func GenerateNewKey(db *storage.Storage) (string, string, error) { 54 | 55 | W = &WalletSigner{ 56 | storage: db, 57 | } 58 | 59 | newKey, err := gtks.Generate(gtks.Ed25519) 60 | if err != nil { 61 | log.WithError(err).Error("Failed to generate new key") 62 | return "", "", errors.Wrap(err, "failed to generate new key") 63 | } 64 | 65 | W.wallet = newKey 66 | W.sk = newKey.GetSecretKey() 67 | W.pkh = newKey.PubKey.GetAddress() 68 | 69 | if err := W.SaveSigner(); err != nil { 70 | return "", "", errors.Wrap(err, "Could not save generated key") 71 | } 72 | 73 | return W.sk, W.pkh, nil 74 | } 75 | 76 | // ImportSecretKey Imports a secret key, saves to DB, and sets signer type to wallet 77 | func ImportSecretKey(iEdsk string, db *storage.Storage) (string, string, error) { 78 | 79 | W = &WalletSigner{ 80 | storage: db, 81 | } 82 | 83 | importKey, err := gtks.FromBase58(iEdsk, gtks.Ed25519) 84 | if err != nil { 85 | log.WithError(err).Error("Failed to import key") 86 | return "", "", err 87 | } 88 | 89 | W.wallet = importKey 90 | W.sk = iEdsk 91 | W.pkh = importKey.PubKey.GetAddress() 92 | 93 | if err := W.SaveSigner(); err != nil { 94 | return "", "", errors.Wrap(err, "Could not save imported key") 95 | } 96 | 97 | return W.sk, W.pkh, nil 98 | } 99 | 100 | // Saves Sk/Pkh to DB 101 | func (s *WalletSigner) SaveSigner() error { 102 | 103 | if err := s.storage.SetDelegate(s.sk, s.pkh); err != nil { 104 | return errors.Wrap(err, "Unable to save key/wallet") 105 | } 106 | 107 | if err := s.storage.SetSignerType(SIGNER_WALLET); err != nil { 108 | return errors.Wrap(err, "Unable to save key/wallet") 109 | } 110 | 111 | return nil 112 | } 113 | 114 | func (s *WalletSigner) SignBytes(opBytes []byte) (string, error) { 115 | 116 | // Returns 'Signature' object 117 | sig, err := s.wallet.SignRawBytes(opBytes) 118 | if err != nil { 119 | return "", errors.Wrap(err, "Failed wallet signer") 120 | } 121 | 122 | return sig.ToBase58(), nil 123 | } 124 | 125 | func (s *WalletSigner) GetPublicKey() (string, string, error) { 126 | return s.wallet.PubKey.GetPublicKey(), s.pkh, nil 127 | } 128 | -------------------------------------------------------------------------------- /payouts/delegator_reward.go: -------------------------------------------------------------------------------- 1 | package payouts 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/pkg/errors" 7 | 8 | bolt "go.etcd.io/bbolt" 9 | 10 | "bakinbacon/storage" 11 | ) 12 | 13 | type DelegatorReward struct { 14 | Delegator string `json:"d"` 15 | Balance int `json:"b"` 16 | SharePct float64 `json:"p"` 17 | Reward int `json:"r"` 18 | OpHash string `json:"o"` 19 | } 20 | 21 | func (p *PayoutsHandler) GetDelegatorRewardForCycle(address string, cycle int) (DelegatorReward, error) { 22 | 23 | var delegatorReward DelegatorReward 24 | 25 | err := p.storage.View(func(tx *bolt.Tx) error { 26 | b := tx.Bucket([]byte(DB_PAYOUTS_BUCKET)).Bucket(storage.Itob(cycle)) 27 | if b == nil { 28 | return errors.New("Unable to locate cycle payouts bucket") 29 | } 30 | 31 | delegatorRewardBytes := b.Get([]byte(address)) 32 | if err := json.Unmarshal(delegatorRewardBytes, &delegatorReward); err != nil { 33 | return errors.Wrap(err, "Unable to decode delegator reward info") 34 | } 35 | 36 | return nil 37 | }) 38 | 39 | return delegatorReward, err 40 | } 41 | 42 | func (p *PayoutsHandler) SaveDelegatorReward(rewardCycle int, rewardRecord DelegatorReward) error { 43 | 44 | return p.storage.Update(func(tx *bolt.Tx) error { 45 | b, err := tx.Bucket([]byte(DB_PAYOUTS_BUCKET)).CreateBucketIfNotExists(storage.Itob(rewardCycle)) 46 | if err != nil { 47 | return errors.New("Unable to locate cycle payouts bucket") 48 | } 49 | 50 | rewardRecordBytes, err := json.Marshal(rewardRecord) 51 | if err != nil { 52 | return errors.Wrap(err, "Unable to encode delegator reward") 53 | } 54 | 55 | // Store the record as the value of the record address (key) 56 | // This will allow for easier scanning/searching for a payment record 57 | return b.Put([]byte(rewardRecord.Delegator), rewardRecordBytes) 58 | }) 59 | } 60 | 61 | // Returns all rewards data for each delegator for a specific cycle. Mainly used by the UI when 62 | // refreshing paid/unpaid status during a payouts action 63 | func (p *PayoutsHandler) GetDelegatorRewardAllForCycle(cycle int) (map[string]DelegatorReward, error) { 64 | 65 | // key is delegator address 66 | delegatorRewards := make(map[string]DelegatorReward) 67 | 68 | err := p.storage.View(func(tx *bolt.Tx) error { 69 | b := tx.Bucket([]byte(DB_PAYOUTS_BUCKET)).Bucket(storage.Itob(cycle)) 70 | if b == nil { 71 | return errors.New("Unable to locate cycle payouts bucket") 72 | } 73 | 74 | c := b.Cursor() 75 | 76 | for k, v := c.First(); k != nil; k, v = c.Next() { 77 | if string(k) == DB_METADATA { 78 | continue 79 | } 80 | 81 | // Add the value (JSON object) to the map 82 | var tmpRewards DelegatorReward 83 | if err := json.Unmarshal(v, &tmpRewards); err != nil { 84 | return errors.Wrap(err, "Unable to parse delegator rewards") 85 | } 86 | delegatorRewards[string(k)] = tmpRewards 87 | } 88 | 89 | return nil 90 | }) 91 | 92 | return delegatorRewards, err 93 | } 94 | 95 | // updateDelegatorRewardOpHash will fetch a DelegatorReward from DB via cycle and address, then 96 | // updates, and save the new opHash. 97 | func (p *PayoutsHandler) updateDelegatorRewardOpHash(address string, cycle int, opHash string) error { 98 | 99 | // Fetch from DB 100 | delegatorReward, err := p.GetDelegatorRewardForCycle(address, cycle) 101 | if err != nil { 102 | return err 103 | } 104 | delegatorReward.OpHash = opHash 105 | 106 | return p.SaveDelegatorReward(cycle, delegatorReward) 107 | } 108 | -------------------------------------------------------------------------------- /webserver/api_payouts.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/pkg/errors" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func (ws *WebServer) getPayouts(w http.ResponseWriter, r *http.Request) { 14 | 15 | log.Trace("API - getPayouts") 16 | 17 | payoutsData := make(map[string]interface{}, 2) 18 | payoutsData["status"] = "ok" 19 | 20 | // Check if payouts are disabled 21 | if ws.payoutsHandler.Disabled { 22 | payoutsData["status"] = "disabled" 23 | } 24 | 25 | // Get all rewards metadata from DB 26 | payoutsMetadata, err := ws.payoutsHandler.GetPayoutsMetadataAll() 27 | if err != nil { 28 | log.WithError(err).Error("API - getPayouts") 29 | apiError(errors.Wrap(err, "Unable to get metadata from DB"), w) 30 | 31 | return 32 | } 33 | 34 | payoutsData["metadata"] = payoutsMetadata 35 | 36 | // return raw JSON 37 | w.Header().Set("Content-Type", "application/json") 38 | if err := json.NewEncoder(w).Encode(payoutsData); err != nil { 39 | log.WithError(err).Error("UI Return getPayouts Failure") 40 | } 41 | } 42 | 43 | // getCyclePayouts will return a map[string] containing the cycle's metadata, 44 | // and the individual rewards payouts data 45 | func (ws *WebServer) getCyclePayouts(w http.ResponseWriter, r *http.Request) { 46 | 47 | log.Trace("API - getCyclePayouts") 48 | 49 | // Get query parameter 50 | keys := r.URL.Query() 51 | payoutsCycle, err := strconv.Atoi(keys.Get("c")) 52 | if err != nil { 53 | log.WithError(err).Error("Unable to parse cycle") 54 | apiError(errors.Wrap(err, "Unable to parse cycle"), w) 55 | 56 | return 57 | } 58 | 59 | // Fetch cycle metadata from DB 60 | cycleMetadata, err := ws.payoutsHandler.GetRewardMetadataForCycle(payoutsCycle) 61 | if err != nil { 62 | log.WithError(err).Error("API - getCyclePayouts") 63 | apiError(errors.Wrap(err, "Unable to get cycle metadata from DB"), w) 64 | 65 | return 66 | } 67 | 68 | // Fetch cycle payout data from DB 69 | payoutsData, err := ws.payoutsHandler.GetDelegatorRewardAllForCycle(payoutsCycle) 70 | if err != nil { 71 | log.WithError(err).Error("API - getCyclePayouts") 72 | apiError(errors.Wrap(err, "Unable to get cycle payout from DB"), w) 73 | 74 | return 75 | } 76 | 77 | cyclePayoutsData := make(map[string]interface{}, 2) 78 | cyclePayoutsData["metadata"] = cycleMetadata 79 | cyclePayoutsData["payouts"] = payoutsData 80 | 81 | // return raw JSON 82 | w.Header().Set("Content-Type", "application/json") 83 | if err := json.NewEncoder(w).Encode(cyclePayoutsData); err != nil { 84 | log.WithError(err).Error("UI Return getCyclePayouts Failure") 85 | } 86 | } 87 | 88 | func (ws *WebServer) sendCyclePayouts(w http.ResponseWriter, r *http.Request) { 89 | 90 | log.Trace("API - sendCyclePayouts") 91 | 92 | // Get query parameter 93 | k := make(map[string]int) 94 | if err := json.NewDecoder(r.Body).Decode(&k); err != nil { 95 | apiError(errors.Wrap(err, "Cannot decode body for sendCyclePayouts"), w) 96 | 97 | return 98 | } 99 | 100 | payoutsCycle, ok := k["cycle"] 101 | if !ok { 102 | apiError(errors.New("missing cycle parameter for sending payouts"), w) 103 | 104 | return 105 | } 106 | 107 | // Execute the payouts process 108 | if err := ws.payoutsHandler.SendCyclePayouts(payoutsCycle); err != nil { 109 | log.WithError(err).Error("Unable send cycle payouts") 110 | apiError(errors.Wrap(err, "Unable send cycle payouts"), w) 111 | 112 | return 113 | } 114 | 115 | apiReturnOk(w) 116 | } 117 | -------------------------------------------------------------------------------- /notifications/telegram.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/pkg/errors" 12 | 13 | log "github.com/sirupsen/logrus" 14 | 15 | "bakinbacon/storage" 16 | ) 17 | 18 | type NotifyTelegram struct { 19 | ChatIds []int `json:"chatids"` 20 | ApiKey string `json:"apikey"` 21 | Enabled bool `json:"enabled"` 22 | 23 | storage *storage.Storage 24 | } 25 | 26 | // NewTelegram creates a new NotifyTelegram object using a JSON byte-stream 27 | // provided from either DB lookup or web UI. The stream is unmarshaled into 28 | // a new object which is returned. 29 | // 30 | // If saveConfig is true, save the new object's config to DB. Normally would not 31 | // do this if we just loaded from DB on app startup, but would want to do this 32 | // after getting new config from web UI. 33 | func (n *NotificationHandler) NewTelegram(config []byte, saveConfig bool) (*NotifyTelegram, error) { 34 | 35 | nt := &NotifyTelegram{} 36 | 37 | // empty config from db? 38 | if config != nil { 39 | if err := json.Unmarshal(config, nt); err != nil { 40 | return nt, errors.Wrap(err, "Unable to unmarshal telegram config") 41 | } 42 | } 43 | 44 | // save local reference to database 45 | nt.storage = n.storage 46 | 47 | if saveConfig { 48 | if err := nt.SaveConfig(); err != nil { 49 | return nt, err 50 | } 51 | } 52 | 53 | return nt, nil 54 | } 55 | 56 | func (n *NotifyTelegram) IsEnabled() bool { 57 | return n.Enabled 58 | } 59 | 60 | func (n *NotifyTelegram) Send(msg string) { 61 | 62 | // curl -G \ 63 | // --data-urlencode "chat_id=111112233" \ 64 | // --data-urlencode "text=$message" \ 65 | // https://api.telegram.org/bot${TOKEN}/sendMessage 66 | 67 | req, err := http.NewRequest("GET", fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", n.ApiKey), nil) 68 | if err != nil { 69 | log.WithError(err).Error("Unable to make Telegram request") 70 | return 71 | } 72 | 73 | req.Header.Add("Content-type", "application/x-www-form-urlencoded") 74 | 75 | // Query parameters 76 | q := req.URL.Query() 77 | q.Add("text", msg) 78 | 79 | // HTTP client 10s timeout 80 | client := &http.Client{ 81 | Timeout: time.Second * 10, 82 | } 83 | 84 | // Loop over chatIds, sending message 85 | for _, chatId := range n.ChatIds { 86 | 87 | q.Set("chat_id", strconv.Itoa(chatId)) 88 | 89 | // Encode URL parameters 90 | req.URL.RawQuery = q.Encode() 91 | 92 | // Execute 93 | resp, err := client.Do(req) 94 | if err != nil { 95 | log.WithFields(log.Fields{ 96 | "ChatId": chatId, 97 | }).WithError(err).Error("Unable to send Telegram message") 98 | } 99 | 100 | defer resp.Body.Close() 101 | body, err := ioutil.ReadAll(resp.Body) 102 | if err != nil { 103 | log.WithFields(log.Fields{ 104 | "ChatId": chatId, 105 | }).WithError(err).Error("Unable to read Telegram API response") 106 | } 107 | 108 | log.WithField("Resp", string(body)).Debug("Telegram Reply") 109 | } 110 | 111 | log.WithField("MSG", msg).Info("Sent Telegram Message") 112 | } 113 | 114 | func (n *NotifyTelegram) SaveConfig() error { 115 | 116 | // Marshal ourselves to []byte and send to storage manager 117 | config, err := json.Marshal(n) 118 | if err != nil { 119 | return errors.Wrap(err, "Unable to marshal telegram config") 120 | } 121 | 122 | if err := n.storage.SaveNotifiersConfig(TELEGRAM, config); err != nil { 123 | return errors.Wrap(err, "Unable to save telegram config") 124 | } 125 | 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /webserver/src/settings/bakersettings.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react'; 2 | 3 | import Button from 'react-bootstrap/Button'; 4 | import Col from 'react-bootstrap/Col'; 5 | import Card from 'react-bootstrap/Card'; 6 | import Form from 'react-bootstrap/Form' 7 | 8 | import ToasterContext from '../toaster.js'; 9 | import { apiRequest } from '../util.js'; 10 | 11 | 12 | const BakerSettings = (props) => { 13 | 14 | const { settings, loadSettings } = props; 15 | 16 | const [ bakerSettings, setBakerSettings] = useState(settings["baker"]); 17 | const addToast = useContext(ToasterContext); 18 | 19 | useEffect(() => { 20 | setBakerSettings(settings["baker"]); 21 | }, [settings]); 22 | 23 | const handleUpdate = (event) => { 24 | setBakerSettings((prev) => ({ 25 | ...prev, 26 | [event.target.name]: event.target.value 27 | })); 28 | } 29 | 30 | const validateBakerFee = () => { 31 | const fee = Number(bakerSettings["bakerfee"]); 32 | if (isNaN(fee)) { 33 | return false 34 | } 35 | if (fee < 1 || fee > 99) { 36 | return false 37 | } 38 | return true; 39 | } 40 | 41 | const updateBakerSettings = () => { 42 | 43 | // Validation 44 | if (!validateBakerFee()) { 45 | addToast({ 46 | title: "Settings Error", 47 | msg: "Baker fee must be an integer value between 1 and 99", 48 | type: "danger", 49 | autohide: 3000, 50 | }); 51 | return 52 | } 53 | 54 | // Validations passed; submit changes 55 | const bakerSettingsApiUrl = window.BASE_URL + "/api/settings/bakersettings" 56 | const postData = bakerSettings; 57 | 58 | const requestOptions = { 59 | method: 'POST', 60 | headers: { 'Content-Type': 'application/json' }, 61 | body: JSON.stringify(postData) 62 | }; 63 | 64 | apiRequest(bakerSettingsApiUrl, requestOptions) 65 | .then(() => { 66 | loadSettings(); 67 | addToast({ 68 | title: "Saved Settings", 69 | msg: "Successfully saved baker settings.", 70 | type: "info", 71 | autohide: 3000, 72 | }); 73 | }) 74 | .catch((errMsg) => { 75 | console.log(errMsg); 76 | addToast({ 77 | title: "Settings Error", 78 | msg: errMsg, 79 | type: "danger", 80 | }); 81 | }); 82 | } 83 | 84 | return ( 85 | <> 86 | 87 | Baker Settings 88 | 89 | The following parameters handle different aspects of running a bakery. 90 | 91 | 92 | handleUpdate(e)} /> 93 | Baker Fee - This is how much your bakery charges delegators. Please use whole numbers without the % sign. 94 | 95 | 96 | 97 | 98 | handleUpdate(e)} > 99 | 100 | 101 | 102 | Block Explorer - Which explorer the UI uses when viewing operations. 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | ) 114 | } 115 | 116 | export default BakerSettings 117 | -------------------------------------------------------------------------------- /bakinbacon_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "bytes" 5 | "crypto/rand" 6 | "encoding/hex" 7 | "os" 8 | "testing" 9 | 10 | "bakinbacon/nonce" 11 | "bakinbacon/util" 12 | 13 | "github.com/bakingbacon/go-tezos/v4/crypto" 14 | "github.com/bakingbacon/go-tezos/v4/keys" 15 | 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | func TestMain(m *testing.M) { 20 | 21 | // Connect to node for tests 22 | // gt, err = gotezos.New("127.0.0.1:18732") 23 | // if err != nil { 24 | // panic(fmt.Sprintf("Unable to connect to network: %s\n", err)) 25 | // } 26 | 27 | log.SetLevel(log.DebugLevel) 28 | 29 | os.Exit(m.Run()) 30 | } 31 | 32 | func TestLoadingKey(t *testing.T) { 33 | 34 | // tz1MTZEJE7YH3wzo8YYiAGd8sgiCTxNRHczR 35 | // pk := "edpkvEbxZAv15SAZAacMAwZxjXToBka4E49b3J1VNrM1qqy5iQfLUx" 36 | sk := "edsk3yXukqCQXjCnS4KRKEiotS7wRZPoKuimSJmWnfH2m3a2krJVdf" 37 | 38 | wallet, err := keys.FromBase58(sk, keys.Ed25519) 39 | if err != nil { 40 | log.WithError(err).Fatal("Failed to load wallet") 41 | os.Exit(1) 42 | } 43 | 44 | t.Logf("Baker PKH: %s\n", wallet.PubKey.GetAddress()) 45 | } 46 | 47 | func TestProofOfWork(t *testing.T) { 48 | 49 | forgedBytes := "00050e7f027173c6c8eda1628b74beba1a4825379d90a818e6c0ea0dba4b8c4dc9f52012c10000000061195ce604e627eb811ac7ec2098304273fea05915c8b02cd9c079e02398204732312bab90000000110000000101000000080000000000050e7e4775bb79657508f01a4efd3e9dd8570a1a6a6b39c45a487cdb56a5c049c18694000142423130000000000000" 50 | 51 | networkConstants, err := util.GetNetworkConstants(util.NETWORK_HANGZHOUNET) 52 | if err != nil { 53 | t.Errorf("Cannot load network constants") 54 | } 55 | 56 | s := BakinBacon{NetworkConstants: networkConstants} 57 | powBytes, _, err := s.powLoop(forgedBytes, len("000142423130000000000000")) 58 | if err != nil { 59 | t.Errorf("PowLoop Failed: %s", err) 60 | } 61 | 62 | if powBytes[len(forgedBytes)-12:] != "0001e4ee0000" { 63 | t.Errorf("Incorrect POW") 64 | } 65 | } 66 | 67 | func TestGenericHash(t *testing.T) { 68 | 69 | seed := "e6d84e1e98a65b2f4551be3cf320f2cb2da38ab7925edb2452e90dd5d2eeeead" 70 | seedBytes, _ := hex.DecodeString(seed) 71 | 72 | nonceHash, err := util.CryptoGenericHash(seedBytes, []byte{}) 73 | if err != nil { 74 | t.Errorf("Unable to hash rand bytes for nonce") 75 | } 76 | 77 | // B58 encode seed hash with nonce prefix 78 | encodedNonce := crypto.B58cencode(nonceHash, nonce.Prefix_nonce) 79 | 80 | t.Logf("Seed: %s\n", seed) 81 | t.Logf("Nonce: %s\n", encodedNonce) 82 | t.Logf("Non-Encoded: %s\n", hex.EncodeToString(nonceHash)) 83 | 84 | if encodedNonce != "nceVSbP3hcecWHY1dYoNUMfyB7gH9S7KbC4hEz3XZK5QCrc5DfFGm" { 85 | t.Errorf("Incorrect nonce from seed") 86 | } 87 | } 88 | 89 | func TestNonce(t *testing.T) { 90 | 91 | randBytes := make([]byte, 32) 92 | if _, err := rand.Read(randBytes); err != nil { 93 | t.Errorf("Unable to read random bytes: %s", err) 94 | } 95 | seed := hex.EncodeToString(randBytes) 96 | seedBytes, _ := hex.DecodeString(seed) 97 | 98 | randBytesHash, err := util.CryptoGenericHash(randBytes, []byte{}) 99 | if err != nil { 100 | log.Errorf("Unable to hash rand bytes for nonce: %s", err) 101 | } 102 | 103 | seedBytesHash, err := util.CryptoGenericHash(seedBytes, []byte{}) 104 | if err != nil { 105 | log.Errorf("Unable to hash rand bytes for nonce: %s", err) 106 | } 107 | 108 | // B58 encode seed hash with nonce prefix 109 | encodedRandBytes := crypto.B58cencode(randBytesHash, nonce.Prefix_nonce) 110 | encodedSeedBytes := crypto.B58cencode(seedBytesHash, nonce.Prefix_nonce) 111 | 112 | t.Logf("Seed: %s\n", seed) 113 | t.Logf("ERB: %s\n", encodedRandBytes) 114 | t.Logf("ESB: %s\n", encodedSeedBytes) 115 | 116 | if encodedRandBytes != encodedSeedBytes { 117 | t.Errorf("Encoded bytes do not match") 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /baconsigner/crypto.go: -------------------------------------------------------------------------------- 1 | package baconsigner 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "math/big" 7 | "reflect" 8 | 9 | "github.com/pkg/errors" 10 | 11 | "github.com/btcsuite/btcutil/base58" 12 | ) 13 | 14 | const ( 15 | alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" 16 | ) 17 | 18 | type prefix []byte 19 | 20 | //nolint:deadcode,unused,varcheck // Keeping these in here for completeness 21 | var ( 22 | // For (de)constructing addresses 23 | tz1prefix prefix = []byte{6, 161, 159} 24 | ktprefix prefix = []byte{2, 90, 121} 25 | edskprefix prefix = []byte{43, 246, 78, 7} 26 | edskprefix2 prefix = []byte{13, 15, 58, 7} 27 | edpkprefix prefix = []byte{13, 15, 37, 217} 28 | edeskprefix prefix = []byte{7, 90, 60, 179, 41} 29 | branchprefix prefix = []byte{1, 52} 30 | chainidprefix prefix = []byte{57, 52, 00} 31 | blockprefix prefix = []byte{1} 32 | endorsementprefix prefix = []byte{2} 33 | genericopprefix prefix = []byte{3} 34 | networkprefix prefix = []byte{87, 82, 0} 35 | ) 36 | 37 | // B58cencode encodes a byte array into base58 with prefix 38 | func B58cencode(payload []byte, prefix prefix) string { 39 | 40 | n := make([]byte, len(prefix) + len(payload)) 41 | 42 | for k := range prefix { 43 | n[k] = prefix[k] 44 | } 45 | 46 | for l := range payload { 47 | n[l+len(prefix)] = payload[l] 48 | } 49 | 50 | b58c := encode(n) 51 | 52 | return b58c 53 | } 54 | 55 | func b58cdecode(payload string, prefix []byte) []byte { 56 | b58c, _ := decode(payload) 57 | return b58c[len(prefix):] 58 | } 59 | 60 | func encode(dataBytes []byte) string { 61 | 62 | // Performing SHA256 twice 63 | sha256hash := sha256.New() 64 | sha256hash.Write(dataBytes) 65 | middleHash := sha256hash.Sum(nil) 66 | sha256hash = sha256.New() 67 | sha256hash.Write(middleHash) 68 | hash := sha256hash.Sum(nil) 69 | 70 | checksum := hash[:4] 71 | dataBytes = append(dataBytes, checksum...) 72 | 73 | // For all the "00" versions or any prepended zeros as base58 removes them 74 | zeroCount := 0 75 | for _, b := range dataBytes { 76 | if b == 0 { 77 | zeroCount++ 78 | } else { 79 | break 80 | } 81 | } 82 | 83 | // Performing base58 encoding 84 | encoded := base58.Encode(dataBytes) 85 | 86 | for i := 0; i < zeroCount; i++ { 87 | encoded = "1" + encoded 88 | } 89 | 90 | return encoded 91 | } 92 | 93 | func decode(encoded string) ([]byte, error) { 94 | 95 | zeroCount := 0 96 | for i := 0; i < len(encoded); i++ { 97 | if encoded[i] == 49 { 98 | zeroCount++ 99 | } else { 100 | break 101 | } 102 | } 103 | 104 | dataBytes, err := b58decode(encoded) 105 | if err != nil { 106 | return []byte{}, err 107 | } 108 | 109 | if len(dataBytes) <= 4 { 110 | return []byte{}, errors.New("invalid decode length") 111 | } 112 | data, checksum := dataBytes[:len(dataBytes)-4], dataBytes[len(dataBytes)-4:] 113 | 114 | for i := 0; i < zeroCount; i++ { 115 | data = append([]byte{0}, data...) 116 | } 117 | 118 | // Performing SHA256 twice to validate checksum 119 | sha256hash := sha256.New() 120 | sha256hash.Write(data) 121 | middleHash := sha256hash.Sum(nil) 122 | sha256hash = sha256.New() 123 | sha256hash.Write(middleHash) 124 | hash := sha256hash.Sum(nil) 125 | 126 | if !reflect.DeepEqual(checksum, hash[:4]) { 127 | return nil, errors.New("data and checksum don't match") 128 | } 129 | 130 | return data, nil 131 | } 132 | 133 | func b58decode(data string) ([]byte, error) { 134 | 135 | decimalData := new(big.Int) 136 | alphabetBytes := []byte(alphabet) 137 | multiplier := big.NewInt(58) 138 | 139 | for _, value := range data { 140 | pos := bytes.IndexByte(alphabetBytes, byte(value)) 141 | if pos == -1 { 142 | return nil, errors.New("character not found in alphabet") 143 | } 144 | decimalData.Mul(decimalData, multiplier) 145 | decimalData.Add(decimalData, big.NewInt(int64(pos))) 146 | } 147 | 148 | return decimalData.Bytes(), nil 149 | } 150 | -------------------------------------------------------------------------------- /notifications/notifications.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | 9 | log "github.com/sirupsen/logrus" 10 | 11 | "bakinbacon/storage" 12 | ) 13 | 14 | type Category int 15 | 16 | const ( 17 | STARTUP Category = iota + 1 18 | BALANCE 19 | SIGNER 20 | BAKING_OK 21 | BAKING_FAIL 22 | ENDORSE_FAIL 23 | VERSION 24 | NONCE 25 | PAYOUTS 26 | 27 | TELEGRAM = "telegram" 28 | EMAIL = "email" 29 | ) 30 | 31 | type Notifier interface { 32 | Send(string) 33 | IsEnabled() bool 34 | } 35 | 36 | type NotificationHandler struct { 37 | notifiers map[string]Notifier 38 | lastSentCategory map[Category]time.Time 39 | storage *storage.Storage 40 | } 41 | 42 | func NewHandler(db *storage.Storage) (*NotificationHandler, error) { 43 | 44 | n := &NotificationHandler{ 45 | notifiers: make(map[string]Notifier), 46 | lastSentCategory: make(map[Category]time.Time), 47 | storage: db, 48 | } 49 | 50 | if err := n.LoadNotifiers(); err != nil { 51 | return nil, errors.Wrap(err, "Failed to instantiate notification handler") 52 | } 53 | 54 | log.Debug("Loaded notifications handler") 55 | 56 | return n, nil 57 | } 58 | 59 | func (n *NotificationHandler) LoadNotifiers() error { 60 | 61 | // Get telegram notifications config from DB, as []byte string 62 | telegramConfig, err := n.storage.GetNotifiersConfig(TELEGRAM) 63 | if err != nil { 64 | return errors.Wrap(err, "Unable to load telegram config") 65 | } 66 | 67 | // Configure telegram; Don't save what we just loaded 68 | if err := n.Configure(TELEGRAM, telegramConfig, false); err != nil { 69 | return errors.Wrap(err, "Unable to init telegram") 70 | } 71 | 72 | // Get email notifications config from DB 73 | emailConfig, err := n.storage.GetNotifiersConfig(EMAIL) 74 | if err != nil { 75 | return errors.Wrap(err, "Unable to load email config") 76 | } 77 | 78 | // Configure email; Don't save what we just loaded 79 | if err := n.Configure(EMAIL, emailConfig, false); err != nil { 80 | return errors.Wrap(err, "Unable to init email") 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func (n *NotificationHandler) Configure(notifier string, config []byte, saveConfig bool) error { 87 | 88 | switch notifier { 89 | case TELEGRAM: 90 | nt, err := n.NewTelegram(config, saveConfig) 91 | if err != nil { 92 | return err 93 | } 94 | n.notifiers[TELEGRAM] = nt 95 | 96 | case EMAIL: 97 | ne, err := n.NewEmail(config, saveConfig) 98 | if err != nil { 99 | return err 100 | } 101 | n.notifiers[EMAIL] = ne 102 | 103 | default: 104 | return errors.New("Unknown notification type") 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (n *NotificationHandler) SendNotification(message string, category Category) { 111 | 112 | // Check that we haven't sent a message from this category 113 | // within the past 10 minutes 114 | if lastSentTime, ok := n.lastSentCategory[category]; ok { 115 | if lastSentTime.After(time.Now().UTC().Add(time.Minute * -10)) { 116 | log.Info("Notification last sent within 10 minutes") 117 | return 118 | } 119 | } 120 | 121 | // Add/update notification timestamp for category 122 | n.lastSentCategory[category] = time.Now().UTC() 123 | 124 | for k, n := range n.notifiers { 125 | if n.IsEnabled() { 126 | n.Send(message) 127 | } else { 128 | log.Infof("Notifications for '%s' are disabled", k) 129 | } 130 | } 131 | } 132 | 133 | func (n *NotificationHandler) TestSend(notifier string, message string) error { 134 | 135 | switch notifier { 136 | case TELEGRAM: 137 | n.notifiers[TELEGRAM].Send(message) 138 | case EMAIL: 139 | n.notifiers[EMAIL].Send(message) 140 | default: 141 | return errors.New("Unknown notification type") 142 | } 143 | 144 | return nil 145 | } 146 | 147 | func (n *NotificationHandler) GetConfig() (json.RawMessage, error) { 148 | 149 | // Marshal the current Notifiers as the current config 150 | // Return RawMessage so as not to double Marshal 151 | bts, err := json.Marshal(n.notifiers) 152 | return json.RawMessage(bts), err 153 | } 154 | -------------------------------------------------------------------------------- /webserver/src/wizards/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Alert from 'react-bootstrap/Alert' 4 | import Button from 'react-bootstrap/Button'; 5 | import Col from 'react-bootstrap/Col'; 6 | import Card from 'react-bootstrap/Card'; 7 | import Row from 'react-bootstrap/Row'; 8 | 9 | import WizardWallet from './wallet.js'; 10 | import WizardLedger from './ledger.js'; 11 | 12 | // -- 13 | // -- Main Wizard Class 14 | // -- 15 | 16 | const SetupWizard = (props) => { 17 | 18 | const { didEnterWizard } = props; 19 | 20 | const [ wizardType, setWizardtype ] = useState(""); 21 | 22 | const selectWizard = (opt) => { 23 | didEnterWizard(true); // Tells parent component to continue displaying wizard even after delegate is set 24 | setWizardtype(opt); 25 | } 26 | 27 | const finishWizard = () => { 28 | setWizardtype("fin"); 29 | } 30 | 31 | return ( 32 | 33 | 34 | 35 | Setup Wizard 36 | 37 | 38 | { !wizardType && 39 | <> 40 | Welcome to Bakin'Bacon! 41 | It appears that you have not configured Bakin'Bacon, so let's do that now. 42 | You first need to decide where to store your super-secret private key using for baking on the Tezos blockchain. You have two choices, listed below, along with some pros/cons for each option: 43 | 44 |
    45 |
  • Software Wallet 46 |
      47 |
    • Pro: Built-in; No external hardware
    • 48 |
    • Pro: Can export private key for backup
    • 49 |
    • Pro: Automated rewards payouts to delegators
    • 50 |
    • Con: Not as secure as hardware-based solutions
    • 51 |
    52 |
  • 53 |
  • Ledger Device 54 |
      55 |
    • Pro: Ultra-secure device, proven in the industry
    • 56 |
    • Pro: Physical confirmation required for any transaction
    • 57 |
    • Con: No automated rewards; must physically process rewards
    • 58 |
    • Con: External hardware component creates additional dependencies
    • 59 |
    60 |
  • 61 |
62 | 63 | We highly recommend the use of a ledger device for maximum security. 64 | 65 | Please select your choice by clicking on one of the buttons below: 66 | 67 | WARNING: This choice is permanent! If you pick software wallet now, you cannot switch to ledger in the future, as ledger does not support importing keys. Similarly, if you pick Ledger now you cannot switch to software wallet, as ledger does not allow you to export keys. 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | } 76 | 77 | { wizardType === "wallet" && } 78 | { wizardType === "ledger" && } 79 | 80 | { wizardType === "fin" && 81 | <> 82 | Setup Complete 83 | Congratulations! You have set up Bakin'Bacon. 84 | Now that you have an address for use on the Tezos blockchain, you will need to fund this address with a minimum of 8,001 XTZ in order to become a baker. 85 | For every 8,000 XTZ in your address, the network grants you 1 roll. In simplistic terms, at the start of every cycle, the blockchain determines how many rolls each baker has and randomly assigns baking rights based on how many each baker has. The more rolls you have, the more chances you have to earn baking and endorsing rights. 86 | There is no guarantee you will get rights every cycle. It is pure random chance. This is one aspect that makes Tezos hard to take advantage of by malicious attackers. 87 | You can refresh this page to see your status. 88 | 89 | } 90 | 91 |
92 |
93 | 94 |
95 | ) 96 | } 97 | 98 | export default SetupWizard 99 | -------------------------------------------------------------------------------- /payouts/cycle_rewards_metadata.go: -------------------------------------------------------------------------------- 1 | package payouts 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/pkg/errors" 7 | 8 | bolt "go.etcd.io/bbolt" 9 | 10 | "bakinbacon/storage" 11 | ) 12 | 13 | const ( 14 | CALCULATED = "calc" 15 | DONE = "done" 16 | IN_PROGRESS = "inprog" 17 | ERROR = "err" 18 | ) 19 | 20 | type CycleRewardMetadata struct { 21 | PayoutCycle int `json:"c"` // Rewards cycle 22 | LevelOfPayoutCycle int `json:"lpc"` // First level of rewards cycle 23 | SnapshotIndex int `json:"si"` // Index of snapshot used for reward cycle 24 | SnapshotLevel int `json:"sl"` // Level of the snapshot used for reward cycle 25 | UnfrozenLevel int `json:"ul"` // Last block of cycle where rewards are unfrozen 26 | 27 | BakerFee float64 `json:"f"` // Fee of baker at time of processing 28 | NumDelegators int `json:"nd"` // Number of delegators 29 | 30 | Balance int `json:"b"` // Balance of baker at time of snapshot 31 | StakingBalance int `json:"sb"` // Staking balance of baker (includes bakers own balance) 32 | DelegatedBalance int `json:"db"` // Delegated balance of baker 33 | BlockRewards int `json:"br"` // Rewards for all bakes/endorses 34 | FeeRewards int `json:"fr"` // Rewards for all transaction fees included in our blocks 35 | 36 | Status string `json:"st"` // One of: calculated, done, or in-progress 37 | } 38 | 39 | // GetPayoutsMetadataAll returns a map of CycleRewardsMetadata 40 | func (p *PayoutsHandler) GetPayoutsMetadataAll() (map[int]CycleRewardMetadata, error) { 41 | 42 | payoutsMetadata := make(map[int]CycleRewardMetadata) 43 | 44 | err := p.storage.View(func(tx *bolt.Tx) error { 45 | b := tx.Bucket([]byte(DB_PAYOUTS_BUCKET)) 46 | if b == nil { 47 | return errors.New("Unable to locate cycle payouts bucket") 48 | } 49 | 50 | c := b.Cursor() 51 | 52 | for k, _ := c.First(); k != nil; k, _ = c.Next() { 53 | 54 | // keys are cycle numbers, which are buckets of data 55 | cycleBucket := b.Bucket(k) 56 | cycle := storage.Btoi(k) 57 | 58 | // Get metadata key from bucket 59 | metadataBytes := cycleBucket.Get([]byte(DB_METADATA)) 60 | 61 | // Unmarshal ... 62 | var tmpMetadata CycleRewardMetadata 63 | if err := json.Unmarshal(metadataBytes, &tmpMetadata); err != nil { 64 | return errors.Wrap(err, "Unable to fetch metadata") 65 | } 66 | 67 | // ... and add to map 68 | payoutsMetadata[cycle] = tmpMetadata 69 | } 70 | 71 | return nil 72 | }) 73 | 74 | return payoutsMetadata, err 75 | } 76 | 77 | func (p *PayoutsHandler) setCyclePayoutStatus(cycle int, status string) error { 78 | 79 | // Fetch, update, save 80 | metadata, err := p.GetRewardMetadataForCycle(cycle) 81 | if err != nil { 82 | return err 83 | } 84 | metadata.Status = status 85 | 86 | if err := p.SaveRewardMetadataForCycle(cycle, metadata); err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | 93 | // GetRewardMetadataForCycle returns metadata struct for a single cycle 94 | func (p *PayoutsHandler) GetRewardMetadataForCycle(rewardCycle int) (CycleRewardMetadata, error) { 95 | 96 | var cycleMetadata CycleRewardMetadata 97 | 98 | err := p.storage.View(func(tx *bolt.Tx) error { 99 | b := tx.Bucket([]byte(DB_PAYOUTS_BUCKET)).Bucket(storage.Itob(rewardCycle)) 100 | if b == nil { 101 | // No bucket for cycle; Return empty metadata for creation 102 | return nil 103 | } 104 | 105 | // Get metadata key from bucket 106 | cycleMetadataBytes := b.Get([]byte(DB_METADATA)) 107 | 108 | // No data, can't unmarshal; Return empty metadata for creation 109 | if len(cycleMetadataBytes) == 0 { 110 | return nil 111 | } 112 | 113 | // Unmarshal ... 114 | if err := json.Unmarshal(cycleMetadataBytes, &cycleMetadata); err != nil { 115 | return errors.Wrap(err, "Unable to unmarshal cycle metadata") 116 | } 117 | 118 | return nil 119 | }) 120 | 121 | return cycleMetadata, err 122 | } 123 | 124 | func (p *PayoutsHandler) SaveRewardMetadataForCycle(rewardCycle int, metadata CycleRewardMetadata) error { 125 | 126 | // Marshal to bytes 127 | metadataBytes, err := json.Marshal(metadata) 128 | if err != nil { 129 | return errors.Wrap(err, "Unable to save reward metadata for cycle") 130 | } 131 | 132 | return p.storage.Update(func(tx *bolt.Tx) error { 133 | b, err := tx.Bucket([]byte(DB_PAYOUTS_BUCKET)).CreateBucketIfNotExists(storage.Itob(rewardCycle)) 134 | if err != nil { 135 | return errors.New("Unable to create cycle payouts bucket") 136 | } 137 | 138 | return b.Put([]byte(DB_METADATA), metadataBytes) 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /webserver/api_wizard.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/pkg/errors" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // 13 | // Test existence of ledger device and get app version (Step 1) 14 | func (ws *WebServer) testLedger(w http.ResponseWriter, r *http.Request) { 15 | 16 | log.Debug("API - TestLedger") 17 | 18 | ledgerInfo, err := ws.baconClient.Signer.TestLedger() 19 | if err != nil { 20 | apiError(errors.Wrap(err, "Unable to access ledger"), w) 21 | return 22 | } 23 | 24 | // Return back to UI 25 | if err := json.NewEncoder(w).Encode(ledgerInfo); err != nil { 26 | log.WithError(err).Error("UI Return Encode Failure") 27 | } 28 | } 29 | 30 | // 31 | // Ledger: confirm the current bipPath and associated key 32 | func (ws *WebServer) confirmBakingPkh(w http.ResponseWriter, r *http.Request) { 33 | 34 | log.Debug("API - ConfirmBakingPkh") 35 | 36 | k := make(map[string]string) 37 | 38 | if err := json.NewDecoder(r.Body).Decode(&k); err != nil { 39 | apiError(errors.Wrap(err, "Cannot decode body for bipPath"), w) 40 | return 41 | } 42 | 43 | // Confirming will prompt user on device to push button, 44 | // also saves config to DB on success 45 | if err := ws.baconClient.Signer.ConfirmBakingPkh(k["pkh"], k["bp"]); err != nil { 46 | apiError(err, w) 47 | return 48 | } 49 | 50 | // Update bacon status so when user refreshes page it is updated 51 | // non-silent checks (silent = false) 52 | _ = ws.baconClient.CanBake(false) 53 | 54 | // Return to UI 55 | apiReturnOk(w) 56 | } 57 | 58 | // 59 | // Generate new key 60 | // Save generated key to database, and set signer type to wallet 61 | func (ws *WebServer) generateNewKey(w http.ResponseWriter, r *http.Request) { 62 | 63 | log.Debug("API - GenerateNewKey") 64 | 65 | // Generate new key temporarily 66 | newEdsk, newPkh, err := ws.baconClient.Signer.GenerateNewKey() 67 | if err != nil { 68 | apiError(err, w) 69 | return 70 | } 71 | 72 | log.WithField("PKH", newPkh).Info("Generated new key-pair") 73 | 74 | // Return back to UI 75 | if err := json.NewEncoder(w).Encode(map[string]string{ 76 | "edsk": newEdsk, 77 | "pkh": newPkh, 78 | }); err != nil { 79 | log.WithError(err).Error("UI Return Encode Failure") 80 | } 81 | } 82 | 83 | // 84 | // Import a secret key 85 | // Save imported key to database, and set signer type to wallet 86 | func (ws *WebServer) importSecretKey(w http.ResponseWriter, r *http.Request) { 87 | 88 | log.Debug("API - ImportSecretKey") 89 | 90 | // CORS crap; Handle OPTION preflight check 91 | if r.Method == http.MethodOptions { 92 | return 93 | } 94 | 95 | k := make(map[string]string) 96 | 97 | if err := json.NewDecoder(r.Body).Decode(&k); err != nil { 98 | apiError(errors.Wrap(err, "Cannot decode body for secret key import"), w) 99 | return 100 | } 101 | 102 | // Imports key temporarily 103 | edsk, pkh, err := ws.baconClient.Signer.ImportSecretKey(k["edsk"]) 104 | if err != nil { 105 | apiError(err, w) 106 | return 107 | } 108 | 109 | log.WithField("PKH", pkh).Info("Imported secret key-pair") 110 | 111 | // Return back to UI 112 | if err := json.NewEncoder(w).Encode(map[string]string{ 113 | "edsk": edsk, 114 | "pkh": pkh, 115 | }); err != nil { 116 | log.WithError(err).Error("UI Return Encode Failure") 117 | } 118 | } 119 | 120 | // 121 | // Call baconClient.RegisterBaker() to construct and inject registration operation. 122 | // This will also check if reveal is needed. 123 | func (ws *WebServer) registerBaker(w http.ResponseWriter, r *http.Request) { 124 | 125 | log.Debug("API - Registerbaker") 126 | 127 | // CORS crap; Handle OPTION preflight check 128 | if r.Method == http.MethodOptions { 129 | return 130 | } 131 | 132 | opHash, err := ws.baconClient.RegisterBaker() 133 | if err != nil { 134 | apiError(errors.Wrap(err, "Cannot register baker"), w) 135 | return 136 | } 137 | 138 | log.WithFields(log.Fields{ 139 | "OpHash": opHash, 140 | }).Info("Injected registration operation") 141 | 142 | // Return to UI 143 | if err := json.NewEncoder(w).Encode(map[string]string{ 144 | "ophash": opHash, 145 | }); err != nil { 146 | log.WithError(err).Error("UI Return Encode Failure") 147 | } 148 | } 149 | 150 | // 151 | // Finish wallet wizard 152 | // This API saves the generated, or imported, secret key to the DB and saves the signer method 153 | func (ws *WebServer) finishWalletWizard(w http.ResponseWriter, r *http.Request) { 154 | 155 | log.Debug("API - FinishWalletWizard") 156 | 157 | if err := ws.baconClient.Signer.SaveSigner(); err != nil { 158 | apiError(errors.Wrap(err, "Cannot save key/wallet to db"), w) 159 | return 160 | } 161 | 162 | // Return to UI 163 | apiReturnOk(w) 164 | } 165 | -------------------------------------------------------------------------------- /webserver/src/settings/rpcservers.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react'; 2 | 3 | import Button from 'react-bootstrap/Button'; 4 | import Col from 'react-bootstrap/Col'; 5 | import Card from 'react-bootstrap/Card'; 6 | import Form from 'react-bootstrap/Form' 7 | import ListGroup from 'react-bootstrap/ListGroup'; 8 | 9 | import ToasterContext from '../toaster.js'; 10 | import { CHAINIDS, apiRequest } from '../util.js'; 11 | 12 | 13 | const Rpcservers = (props) => { 14 | 15 | const { settings, loadSettings } = props; 16 | 17 | const [newRpc, setNewRpc] = useState(""); 18 | const [rpcEndpoints, setRpcEndpoints] = useState({}); 19 | const addToast = useContext(ToasterContext); 20 | 21 | useEffect(() => { 22 | setRpcEndpoints(settings.endpoints); 23 | }, [settings]); 24 | 25 | const handleNewRpcChange = (event) => { 26 | setNewRpc(event.target.value); 27 | } 28 | 29 | const addRpc = () => { 30 | 31 | // Cheezy sanity check 32 | const rpcToAdd = stripSlash(newRpc); 33 | if (rpcToAdd.length < 10) { 34 | addToast({ 35 | title: "Add RPC Error", 36 | msg: "That does not appear a valid URL", 37 | type: "warning", 38 | autohide: 3000, 39 | }); 40 | return; 41 | } 42 | 43 | console.log("Adding RPC endpoint: " + rpcToAdd) 44 | 45 | // Sanity check the endpoint first by fetching the current head and checking the protocol. 46 | // This has the added effect of forcing upgrades for new protocols. 47 | apiRequest(rpcToAdd + "/chains/main/blocks/head/header") 48 | .then((data) => { 49 | const rpcChainId = data.chain_id; 50 | const networkChainId = CHAINIDS[window.NETWORK] 51 | if (rpcChainId !== networkChainId) { 52 | throw new Error("RPC chain ("+rpcChainId+") does not match "+networkChainId+". Please use a correct RPC server."); 53 | } 54 | 55 | // RPC is good! Add it via API. 56 | const apiUrl = window.BASE_URL + "/api/settings/addendpoint" 57 | const postData = {rpc: rpcToAdd} 58 | handlePostAPI(apiUrl, postData).then(() => { 59 | addToast({ 60 | title: "RPC Success", 61 | msg: "Added RPC Server", 62 | type: "success", 63 | autohide: 3000, 64 | }); 65 | }); 66 | }) 67 | .catch((errMsg) => { 68 | console.log(errMsg); 69 | addToast({ 70 | title: "Add RPC Error", 71 | msg: "There was an error in validating the RPC URL: " + errMsg, 72 | type: "danger", 73 | }); 74 | }) 75 | .finally(() => { 76 | setNewRpc(""); 77 | }); 78 | } 79 | 80 | const delRpc = (rpc) => { 81 | const apiUrl = window.BASE_URL + "/api/settings/deleteendpoint" 82 | const postData = {rpc: Number(rpc)} 83 | handlePostAPI(apiUrl, postData).then(() => { 84 | addToast({ 85 | title: "RPC Success", 86 | msg: "Deleted RPC Server", 87 | type: "success", 88 | autohide: 3000, 89 | }); 90 | }) 91 | .finally(() => { 92 | setNewRpc(""); 93 | }); 94 | } 95 | 96 | // Add/Delete RPC, and Save Telegram/Email RPCs use POST and only care if failure. 97 | // On 200 OK, refresh settings 98 | const handlePostAPI = (url, data) => { 99 | 100 | const requestOptions = { 101 | method: 'POST', 102 | headers: { 'Content-Type': 'application/json' }, 103 | body: JSON.stringify(data) 104 | }; 105 | 106 | return apiRequest(url, requestOptions) 107 | .then(() => { 108 | loadSettings(); 109 | }) 110 | .catch((errMsg) => { 111 | console.log(errMsg); 112 | addToast({ 113 | title: "Settings Error", 114 | msg: errMsg, 115 | type: "danger", 116 | }); 117 | }); 118 | } 119 | 120 | return ( 121 | <> 122 | 123 | RPC Servers 124 | 125 | BakinBacon supports multiple RPC servers for increased redundancy against network issues and will always use the most up-to-date server. 126 | 127 | 128 | { Object.keys(rpcEndpoints).map((rpcId) => { 129 | return {rpcEndpoints[rpcId]} 130 | })} 131 | 132 | 133 | 134 | 135 | 136 | Add RPC Server 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | ) 146 | } 147 | 148 | function stripSlash(d) { 149 | return d.endsWith('/') ? d.substr(0, d.length - 1) : d; 150 | } 151 | 152 | export default Rpcservers 153 | -------------------------------------------------------------------------------- /webserver/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext, useRef } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import Alert from 'react-bootstrap/Alert' 5 | import Col from 'react-bootstrap/Col'; 6 | import Container from 'react-bootstrap/Container'; 7 | import Navbar from 'react-bootstrap/Navbar' 8 | import Row from 'react-bootstrap/Row'; 9 | import Tabs from 'react-bootstrap/Tabs'; 10 | import Tab from 'react-bootstrap/Tab'; 11 | 12 | import BakinDashboard from './dashboard.js' 13 | import DelegateRegister from './delegateregister.js' 14 | import Settings, { GetUiExplorer } from './settings' 15 | import SetupWizard from './wizards' 16 | import Payouts from './payouts' 17 | import Voting from './voting.js' 18 | 19 | import ToasterContext, { ToasterContextProvider } from './toaster.js'; 20 | import { NO_SIGNER, NOT_REGISTERED, apiRequest } from './util.js'; 21 | 22 | import '../node_modules/bootstrap/dist/css/bootstrap.min.css'; 23 | import './index.css'; 24 | 25 | import logo from './logo512.png'; 26 | 27 | 28 | const Bakinbacon = () => { 29 | 30 | const [ delegate, setDelegate ] = useState(""); 31 | const [ status, setStatus ] = useState({}); 32 | const [ lastUpdate, setLastUpdate ] = useState(new Date().toLocaleTimeString()); 33 | const [ uiExplorer, setUiExplorer ] = useState("tzstats"); 34 | const [ connOk, setConnOk ] = useState(false); 35 | const [ isLoading, setIsLoading ] = useState(true); 36 | const [ inWizard, setInWizard ] = useState(false); 37 | 38 | const addToast = useContext(ToasterContext); 39 | 40 | // Hold a reference so we can cancel it externally 41 | const fetchStatusTimer = useRef(); 42 | 43 | // On component load 44 | useEffect(() => { 45 | 46 | setIsLoading(true); 47 | 48 | fetchStatus(); 49 | GetUiExplorer(setUiExplorer); 50 | 51 | // Update every 10 seconds 52 | const idTimer = setInterval(() => fetchStatus(), 10000); 53 | fetchStatusTimer.current = idTimer; 54 | return () => { 55 | // componentWillUnmount() 56 | clearInterval(fetchStatusTimer.current); 57 | }; 58 | // eslint-disable-next-line react-hooks/exhaustive-deps 59 | }, [fetchStatusTimer]); 60 | 61 | // Update the state of being in the wizard from within the wizard 62 | const didEnterWizard = (wizType) => { 63 | setInWizard(wizType); 64 | clearInterval(fetchStatusTimer); 65 | } 66 | 67 | const didEnterRegistration = () => { 68 | // If we need to register as baker, stop fetching /api/status until that completes 69 | clearInterval(fetchStatusTimer); 70 | } 71 | 72 | const fetchStatus = () => { 73 | 74 | const statusApiUrl = window.BASE_URL + "/api/status"; 75 | 76 | apiRequest(statusApiUrl) 77 | .then((statusRes) => { 78 | setDelegate(statusRes.pkh); 79 | setStatus(statusRes); 80 | setLastUpdate(new Date(statusRes.ts * 1000).toLocaleTimeString()); 81 | setConnOk(true); 82 | setIsLoading(false); 83 | }) 84 | .catch((errMsg) => { 85 | console.log(errMsg) 86 | setConnOk(false); 87 | addToast({ 88 | title: "Fetch Dashboard Error", 89 | msg: "Unable to fetch status from BakinBacon ("+errMsg+"). Is the server running?", 90 | type: "danger", 91 | autohide: 10000, 92 | }); 93 | }) 94 | } 95 | 96 | // Returns 97 | if (!isLoading && ((!delegate && status.state === NO_SIGNER) || inWizard)) { 98 | // Need to run setup wizard 99 | return ( 100 | <> 101 | 102 | 103 | 104 | 105 | BakinBacon Logo{' '}Bakin'Bacon 106 | 107 | 108 | 109 | 110 | 111 | 112 | ); 113 | } 114 | 115 | // Done loading; Display 116 | return ( 117 | <> 118 | 119 | 120 | 121 | 122 | BakinBacon Logo{' '}Bakin'Bacon 123 | 124 | {delegate} 125 | 126 | 127 | 128 | 129 | { isLoading ? Loading dashboard... : 130 | 131 | 132 | 133 | 134 | { status.state === NOT_REGISTERED ? 135 | 136 | : 137 | 138 | } 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | } 153 | 154 | 155 | 156 |
Last Update: {lastUpdate} 157 |
158 | 159 |
160 |
161 | 162 | ); 163 | } 164 | 165 | ReactDOM.render(, document.getElementById('bakinbacon')); 166 | -------------------------------------------------------------------------------- /webserver/api_settings.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "github.com/pkg/errors" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func (ws *WebServer) saveBakerSettings(w http.ResponseWriter, r *http.Request) { 14 | 15 | log.Trace("API - saveBakerSettings") 16 | 17 | // From javascript UI, everything is a string 18 | k := make(map[string]string) 19 | 20 | if err := json.NewDecoder(r.Body).Decode(&k); err != nil { 21 | apiError(errors.Wrap(err, "Cannot decode body for baker settings"), w) 22 | return 23 | } 24 | 25 | if err := ws.storage.SaveBakerSettings(k); err != nil { 26 | apiError(errors.Wrap(err, "Cannot save baker settings"), w) 27 | return 28 | } 29 | 30 | apiReturnOk(w) 31 | } 32 | 33 | func (ws *WebServer) saveTelegram(w http.ResponseWriter, r *http.Request) { 34 | 35 | log.Trace("API - SaveTelegram") 36 | 37 | // Read the POST body as a string 38 | body, err := ioutil.ReadAll(r.Body) 39 | if err != nil { 40 | log.WithError(err).Error("API SaveTelegram") 41 | apiError(errors.Wrap(err, "Failed to parse body"), w) 42 | 43 | return 44 | } 45 | 46 | // Send string to configure for JSON unmarshaling; make sure to save config to db 47 | if err := ws.notificationHandler.Configure("telegram", body, true); err != nil { 48 | log.WithError(err).Error("API SaveTelegram") 49 | apiError(errors.Wrap(err, "Failed to configure telegram"), w) 50 | 51 | return 52 | } 53 | 54 | if err := ws.notificationHandler.TestSend("telegram", "Test message from BakinBacon"); err != nil { 55 | log.WithError(err).Error("API SaveTelegram") 56 | apiError(errors.Wrap(err, "Failed to execute telegram test"), w) 57 | 58 | return 59 | } 60 | 61 | apiReturnOk(w) 62 | } 63 | 64 | func (ws *WebServer) saveEmail(w http.ResponseWriter, r *http.Request) { 65 | apiReturnOk(w) 66 | } 67 | 68 | func (ws *WebServer) getSettings(w http.ResponseWriter, r *http.Request) { 69 | 70 | log.Trace("API - GetSettings") 71 | 72 | // Get RPC endpoints 73 | endpoints, err := ws.storage.GetRPCEndpoints() 74 | if err != nil { 75 | apiError(errors.Wrap(err, "Cannot get endpoints"), w) 76 | return 77 | } 78 | log.WithField("Endpoints", endpoints).Debug("API Settings Endpoints") 79 | 80 | // Get Notification settings 81 | notifications, err := ws.notificationHandler.GetConfig() // Returns json.RawMessage 82 | if err != nil { 83 | apiError(errors.Wrap(err, "Cannot get notification settings"), w) 84 | return 85 | } 86 | log.WithField("Notifications", string(notifications)).Debug("API Settings Notifications") 87 | 88 | // Get baker settings 89 | bakerSettings, err := ws.storage.GetBakerSettings() 90 | if err != nil { 91 | apiError(errors.Wrap(err, "Cannot get baker settings"), w) 92 | return 93 | } 94 | log.WithField("Settings", bakerSettings).Debug("API Settings BakerSettings") 95 | 96 | if err := json.NewEncoder(w).Encode(map[string]interface{}{ 97 | "endpoints": endpoints, 98 | "notifications": notifications, 99 | "baker": bakerSettings, 100 | }); err != nil { 101 | log.WithError(err).Error("UI Return Encode Failure") 102 | } 103 | } 104 | 105 | // 106 | // Adding, Listing, Deleting endpoints 107 | func (ws *WebServer) addEndpoint(w http.ResponseWriter, r *http.Request) { 108 | 109 | log.Trace("API - AddEndpoint") 110 | 111 | k := make(map[string]string) 112 | 113 | if err := json.NewDecoder(r.Body).Decode(&k); err != nil { 114 | apiError(errors.Wrap(err, "Cannot decode body for rpc add"), w) 115 | return 116 | } 117 | 118 | // Save new RPC to db to get id 119 | id, err := ws.storage.AddRPCEndpoint(k["rpc"]) 120 | if err != nil { 121 | log.WithError(err).WithField("Endpoint", k).Error("API AddEndpoint") 122 | apiError(errors.Wrap(err, "Cannot add endpoint to DB"), w) 123 | return 124 | } 125 | 126 | // Init new bacon watcher for this RPC 127 | ws.baconClient.AddRpc(id, k["rpc"]) 128 | 129 | log.WithField("Endpoint", k["rpc"]).Debug("API Added Endpoint") 130 | 131 | apiReturnOk(w) 132 | } 133 | 134 | func (ws *WebServer) listEndpoints(w http.ResponseWriter, r *http.Request) { 135 | 136 | log.Trace("API - ListEndpoints") 137 | 138 | endpoints, err := ws.storage.GetRPCEndpoints() 139 | if err != nil { 140 | apiError(errors.Wrap(err, "Cannot get endpoints"), w) 141 | return 142 | } 143 | 144 | log.WithField("Endpoints", endpoints).Debug("API List Endpoints") 145 | 146 | if err := json.NewEncoder(w).Encode(map[string]map[int]string{ 147 | "endpoints": endpoints, 148 | }); err != nil { 149 | log.WithError(err).Error("UI Return Encode Failure") 150 | } 151 | } 152 | 153 | func (ws *WebServer) deleteEndpoint(w http.ResponseWriter, r *http.Request) { 154 | 155 | log.Trace("API - DeleteEndpoint") 156 | 157 | k := make(map[string]int) 158 | 159 | if err := json.NewDecoder(r.Body).Decode(&k); err != nil { 160 | log.WithError(err).Error("Cannot decode body for rpc delete") 161 | apiError(errors.Wrap(err, "Cannot decode body for rpc delete"), w) 162 | 163 | return 164 | } 165 | 166 | // Need to shutdown the RPC client first 167 | if err := ws.baconClient.ShutdownRpc(k["rpc"]); err != nil { 168 | log.WithError(err).WithField("Endpoint", k).Error("API DeleteEndpoint") 169 | apiError(errors.Wrap(err, "Cannot shutdown RPC client for deletion"), w) 170 | 171 | return 172 | } 173 | 174 | // Then delete from storage 175 | if err := ws.storage.DeleteRPCEndpoint(k["rpc"]); err != nil { 176 | log.WithError(err).WithField("Endpoint", k).Error("API DeleteEndpoint") 177 | apiError(errors.Wrap(err, "Cannot delete endpoint from DB"), w) 178 | 179 | return 180 | } 181 | 182 | log.WithField("Endpoint", k["rpc"]).Debug("API Deleted Endpoint") 183 | 184 | apiReturnOk(w) 185 | } 186 | -------------------------------------------------------------------------------- /webserver/src/delegateregister.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react'; 2 | 3 | import Alert from 'react-bootstrap/Alert'; 4 | import Button from 'react-bootstrap/Button'; 5 | import Col from 'react-bootstrap/Col'; 6 | import Card from 'react-bootstrap/Card'; 7 | import Loader from "react-loader-spinner"; 8 | import Row from 'react-bootstrap/Row'; 9 | 10 | import ToasterContext from './toaster.js'; 11 | import { BaconAlert, apiRequest } from './util.js'; 12 | 13 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 14 | 15 | 16 | const DelegateRegister = (props) => { 17 | 18 | const { delegate, didEnterRegistration } = props; 19 | 20 | const [ step, setStep ] = useState(0); 21 | const [ alert, setAlert ] = useState({}) 22 | const [ isLoading, setIsLoading ] = useState(false); 23 | const [ balance, setBalance ] = useState(0); 24 | const addToast = useContext(ToasterContext); 25 | 26 | useEffect(() => { 27 | 28 | didEnterRegistration(); // Tell parent we are in here 29 | 30 | // If not registered, fetch balance every 5min 31 | fetchBalanceInfo(); 32 | 33 | let fetchBalanceInfoTimer = setInterval(() => fetchBalanceInfo(), 1000 * 60 * 5); 34 | return () => { 35 | // componentWillUnmount() 36 | clearInterval(fetchBalanceInfoTimer); 37 | fetchBalanceInfoTimer = null; 38 | }; 39 | // eslint-disable-next-line react-hooks/exhaustive-deps 40 | }, []); 41 | 42 | const registerBaker = () => { 43 | const registerBakerApiUrl = window.BASE_URL + "/api/wizard/registerBaker"; 44 | const requestOptions = { 45 | method: 'POST', 46 | headers: { 'Content-Type': 'application/json' }, 47 | }; 48 | 49 | setIsLoading(true); 50 | 51 | apiRequest(registerBakerApiUrl, requestOptions) 52 | .then((data) => { 53 | console.log("Register OpHash: " + data.ophash); 54 | setIsLoading(false); 55 | setStep(99); 56 | }) 57 | .catch((errMsg) => { 58 | console.log(errMsg) 59 | setIsLoading(false); 60 | setAlert({ 61 | type: "danger", 62 | msg: errMsg, 63 | }); 64 | }); 65 | }; 66 | 67 | // If baker is not yet revealed/registered, we need to monitor basic 68 | // balance so we can display the button when enough funds are available. 69 | // Check every 5 minutes 70 | const fetchBalanceInfo = () => { 71 | 72 | setIsLoading(true); 73 | 74 | const balanceUrl = "http://"+window.NETWORK+"-us.rpc.bakinbacon.io/chains/main/blocks/head/context/contracts/" + delegate 75 | apiRequest(balanceUrl) 76 | .then((data) => { 77 | setBalance((parseInt(data.balance, 10) / 1e6).toFixed(1)); 78 | }) 79 | .catch((errMsg) => { 80 | console.log(errMsg) 81 | addToast({ 82 | title: "Loading Balance Error", 83 | msg: errMsg, 84 | type: "danger", 85 | }); 86 | }) 87 | .finally(() => { 88 | setIsLoading(false); 89 | }) 90 | } 91 | 92 | // Returns 93 | if (step === 99) { 94 | return ( 95 | 96 | Baker Status 97 | 98 | Your baking address, {delegate}, has been registered as a baker! 99 | It is now time to wait, unfortunately. In order to protect against bakers coming and going, the Tezos network will not include your registration for 3 cycles. 100 | After that waiting period, you will begin to receive baking and endorsing opportunities for future cycles. 101 | BakinBacon will always attempt to inject every endorsement you are granted, and only considers priority 0 baking opportunities. 102 | Reload this page to view your baker stats, such as staking balance, and number of delegators. You will also be able to view your next baking and endorsing opportunities when they are granted by the network. 103 | 104 | 105 | ) 106 | } 107 | 108 | // default 109 | return ( 110 | 111 | Baker Status 112 | 113 | { isLoading ? <> 114 | 115 | 116 | 117 | 118 | Submitting baker registration to network. This may take up to 5 minutes to process. Please wait. This page will automatically update when registration has been submitted. 119 | 120 | 121 | If you are using a ledger device, please look at the device and approve the registration. 122 | 123 | : 124 | <> 125 | Your baking address, {delegate}, has not been registered as a baker to the Tezos network. In order to be a baker, you need to have at least 8000 tez in your baking address. 126 | A small, one-time fee of 0.257 XTZ, is also required to register, in addition to standard operation fees. 1 additional tez will cover this. 127 | There is currently {balance} XTZ in your baking address. 128 | 129 | { balance < 8001 ? 130 | Please ensure your balance is at least 8001 XTZ so that we can complete the registration process. 131 | : 132 | <> 133 | 134 | If you are using a ledger device, you will be prompted to confirm this action. Please ensure your device is unlocked and the Tezos Baking application is loaded. 135 | 136 | 137 | 138 | 139 | 140 | } 141 | 142 | } 143 | 144 | 145 | 146 | ) 147 | } 148 | 149 | export default DelegateRegister -------------------------------------------------------------------------------- /storage/rights.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/bakingbacon/go-tezos/v4/rpc" 7 | 8 | "github.com/pkg/errors" 9 | 10 | bolt "go.etcd.io/bbolt" 11 | ) 12 | 13 | const ( 14 | ENDORSING_RIGHTS_BUCKET = "endorsing" 15 | BAKING_RIGHTS_BUCKET = "baking" 16 | ) 17 | 18 | func (s *Storage) SaveEndorsingRightsForCycle(cycle int, endorsingRights []rpc.EndorsingRights) error { 19 | 20 | return s.Update(func(tx *bolt.Tx) error { 21 | 22 | b, err := tx.Bucket([]byte(RIGHTS_BUCKET)).CreateBucketIfNotExists([]byte(ENDORSING_RIGHTS_BUCKET)) 23 | if err != nil { 24 | return errors.Wrap(err, "Unable to create endorsing rights bucket") 25 | } 26 | 27 | // Use the bucket's sequence to save the highest cycle for which rights have been fetched 28 | if err := b.SetSequence(uint64(cycle)); err != nil { 29 | return err 30 | } 31 | 32 | // Keys of values are not related to the sequence 33 | for _, r := range endorsingRights { 34 | if err := b.Put(Itob(r.Level), Itob(cycle)); err != nil { 35 | return err 36 | } 37 | } 38 | 39 | return nil 40 | }) 41 | } 42 | 43 | func (s *Storage) SaveBakingRightsForCycle(cycle int, bakingRights []rpc.BakingRights) error { 44 | 45 | return s.Update(func(tx *bolt.Tx) error { 46 | 47 | b, err := tx.Bucket([]byte(RIGHTS_BUCKET)).CreateBucketIfNotExists([]byte(BAKING_RIGHTS_BUCKET)) 48 | if err != nil { 49 | return errors.Wrap(err, "Unable to create baking rights bucket") 50 | } 51 | 52 | // Use the bucket's sequence to save the highest cycle for which rights have been fetched 53 | if err := b.SetSequence(uint64(cycle)); err != nil { 54 | return err 55 | } 56 | 57 | // Keys of values are not related to the sequence 58 | for _, r := range bakingRights { 59 | if err := b.Put(Itob(r.Level), Itob(r.Priority)); err != nil { 60 | return err 61 | } 62 | } 63 | 64 | return nil 65 | }) 66 | } 67 | 68 | // GetNextEndorsingRight returns the level of the next endorsing opportunity, 69 | // and also the highest cycle for which rights have been previously fetched. 70 | func (s *Storage) GetNextEndorsingRight(curLevel int) (int, int, error) { 71 | 72 | var ( 73 | nextLevel int 74 | highestFetchCycle int 75 | ) 76 | 77 | curLevelBytes := Itob(curLevel) 78 | 79 | err := s.View(func(tx *bolt.Tx) error { 80 | 81 | b := tx.Bucket([]byte(RIGHTS_BUCKET)).Bucket([]byte(ENDORSING_RIGHTS_BUCKET)) 82 | if b == nil { 83 | return errors.New("Endorsing Rights Bucket Not Found") 84 | } 85 | 86 | highestFetchCycle = int(b.Sequence()) 87 | 88 | c := b.Cursor() 89 | 90 | for k, _ := c.First(); k != nil && nextLevel == 0; k, _ = c.Next() { 91 | switch o := bytes.Compare(curLevelBytes, k); o { 92 | case 1, 0: 93 | // k is less than, or equal to current level, loop to next entry 94 | continue 95 | case -1: 96 | nextLevel = Btoi(k) 97 | } 98 | } 99 | 100 | return nil 101 | }) 102 | 103 | // Two conditions can happen. We scanned through all rights: 104 | // 1. .. and found next highest 105 | // 2. .. or found none 106 | 107 | return nextLevel, highestFetchCycle, err 108 | } 109 | 110 | // GetNextBakingRight returns the level of the next baking opportunity, with it's priority, 111 | // and also the highest cycle for which rights have been previously fetched. 112 | func (s *Storage) GetNextBakingRight(curLevel int) (int, int, int, error) { 113 | 114 | var ( 115 | nextLevel int 116 | nextPriority int 117 | highestFetchCycle int 118 | ) 119 | 120 | curLevelBytes := Itob(curLevel) 121 | 122 | err := s.View(func(tx *bolt.Tx) error { 123 | 124 | b := tx.Bucket([]byte(RIGHTS_BUCKET)).Bucket([]byte(BAKING_RIGHTS_BUCKET)) 125 | if b == nil { 126 | return errors.New("Endorsing Rights Bucket Not Found") 127 | } 128 | 129 | highestFetchCycle = int(b.Sequence()) 130 | 131 | c := b.Cursor() 132 | 133 | for k, v := c.First(); k != nil && nextLevel == 0; k, v = c.Next() { 134 | switch o := bytes.Compare(curLevelBytes, k); o { 135 | case 1, 0: 136 | // k is less than, or equal to current level, loop to next entry 137 | continue 138 | case -1: 139 | nextLevel = Btoi(k) 140 | nextPriority = Btoi(v) 141 | } 142 | } 143 | 144 | return nil 145 | }) 146 | 147 | // Two conditions can happen. We scanned through all rights: 148 | // 1. .. and found next highest 149 | // 2. .. or found none 150 | 151 | return nextLevel, nextPriority, highestFetchCycle, err 152 | } 153 | 154 | // GetRecentEndorsement returns the level of the most recent endorsement 155 | func (s *Storage) GetRecentEndorsement() (int, string, error) { 156 | 157 | var ( 158 | recentEndorsementLevel int = 0 159 | recentEndorsementHash string = "" 160 | ) 161 | 162 | err := s.View(func(tx *bolt.Tx) error { 163 | 164 | b := tx.Bucket([]byte(ENDORSING_BUCKET)) 165 | if b == nil { 166 | return errors.New("Endorsing history bucket not found") 167 | } 168 | 169 | // The last/highest key is the most recent endorsement 170 | k, v := b.Cursor().Last() 171 | if k != nil { 172 | recentEndorsementLevel = Btoi(k) 173 | recentEndorsementHash = string(v) 174 | } 175 | 176 | return nil 177 | }) 178 | 179 | return recentEndorsementLevel, recentEndorsementHash, err 180 | } 181 | 182 | // GetRecentBake returns the level of the most recent bake 183 | func (s *Storage) GetRecentBake() (int, string, error) { 184 | 185 | var ( 186 | recentBakeLevel int = 0 187 | recentBakeHash string = "" 188 | ) 189 | 190 | err := s.View(func(tx *bolt.Tx) error { 191 | 192 | b := tx.Bucket([]byte(BAKING_BUCKET)) 193 | if b == nil { 194 | return errors.New("Baking history bucket not found") 195 | } 196 | 197 | // The last/highest key is the most recent endorsement 198 | k, v := b.Cursor().Last() 199 | if k != nil { 200 | recentBakeLevel = Btoi(k) 201 | recentBakeHash = string(v) 202 | } 203 | 204 | return nil 205 | }) 206 | 207 | return recentBakeLevel, recentBakeHash, err 208 | } 209 | -------------------------------------------------------------------------------- /webserver/src/wizards/ledger.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Alert from 'react-bootstrap/Alert'; 4 | import Button from 'react-bootstrap/Button'; 5 | import Card from 'react-bootstrap/Card'; 6 | import Col from 'react-bootstrap/Col'; 7 | import Loader from "react-loader-spinner"; 8 | import Row from 'react-bootstrap/Row'; 9 | 10 | import { BaconAlert, apiRequest } from '../util.js'; 11 | 12 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 13 | 14 | 15 | const WizardLedger = (props) => { 16 | 17 | const { onFinishWizard } = props; 18 | 19 | const [ step, setStep ] = useState(1) 20 | const [ alert, setAlert ] = useState({}) 21 | const [ info, setInfo ] = useState({}) 22 | const [ isLoading, setIsLoading ] = useState(false) 23 | 24 | const testLedger = () => { 25 | // Make API call to UI so BB can check for ledger 26 | 27 | // Clear previous errors 28 | setAlert({}); 29 | setInfo({}); 30 | setIsLoading(true); 31 | 32 | const testLedgerApiUrl = window.BASE_URL + "/api/wizard/testLedger"; 33 | apiRequest(testLedgerApiUrl) 34 | .then((data) => { 35 | // Ledger and baking app detected by BB; enable continue button 36 | console.log(data); 37 | setInfo(data); 38 | setAlert({ 39 | type: "success", 40 | msg: "Detected ledger: " + data.version 41 | }); 42 | setStep(11); 43 | }) 44 | .catch((errMsg) => { 45 | console.log(errMsg); 46 | setAlert({ 47 | type: "danger", 48 | msg: errMsg, 49 | }); 50 | setStep(1); 51 | }) 52 | .finally(() => { 53 | setIsLoading(false); 54 | }); 55 | } 56 | 57 | const stepTwo = () => { 58 | setAlert({ 59 | type: "success", 60 | msg: "Baking Address: " + info.pkh 61 | }); 62 | setStep(2); 63 | } 64 | 65 | const confirmBakingPkh = () => { 66 | 67 | // Still on step 2 68 | setIsLoading(true); 69 | 70 | const confirmPkhApiURL = window.BASE_URL + "/api/wizard/confirmBakingPkh" 71 | const requestOptions = { 72 | method: 'POST', 73 | headers: { 'Content-Type': 'application/json' }, 74 | body: JSON.stringify({ 75 | bp: info.bipPath, 76 | pkh: info.pkh 77 | }) 78 | }; 79 | 80 | apiRequest(confirmPkhApiURL, requestOptions) 81 | .then((data) => { 82 | console.log(data); 83 | setAlert({ 84 | type: "success", 85 | msg: "Yay! Baking address, " + info.pkh + ", confirmed!" 86 | }); 87 | setStep(21); 88 | }) 89 | .catch((errMsg) => { 90 | setAlert({ 91 | type: "danger", 92 | msg: errMsg, 93 | }); 94 | }) 95 | .finally(() => { 96 | setIsLoading(false); 97 | }); 98 | } 99 | 100 | // This renders inside parent 101 | if (step === 1 || step === 11) { 102 | return ( 103 | <> 104 | Setup Ledger Device - Step 1 105 | 106 | 107 | Please make sure that you have completed the following steps before continuing in Bakin'Bacon: 108 |
    109 |
  1. Ensure Ledger device is plugged in to USB port on computer running Bakin'Bacon.
  2. 110 |
  3. Make sure Ledger is unlocked.
  4. 111 |
  5. Install the 'Tezos Baking' application, and ensure it is open.
  6. 112 |
113 | If you do not have the 'Tezos Baking' application installed, you will need to download Ledger Live and use it to install the applications onto your device. 114 | You must successfully test your ledger before continuing. Please click the 'Test Ledger' button below. 115 | 116 |
117 | 118 | 119 | 120 | 121 | 122 | { isLoading && 123 | 124 | Checking for Ledger... 125 | 126 | } 127 | 128 | 129 | 130 | ); 131 | } 132 | 133 | if (step === 2 || step === 21) { 134 | return ( 135 | <> 136 | Setup Ledger Device - Step 2 137 | 138 | 139 | Bakin'Bacon has fetched the key shown below from the ledger device. This is the address that will be used for baking. 140 | You need to confirm this address by clicking the 'Confirm Address' button below, then look at your ledger device, compare the address displayed on the device to the address below, and then click the button on the device to confirm they match. 141 | After you confirm the addresses match, you can then click the "Let's Bake!" button. 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | { isLoading && <> 150 | 151 | 152 | 153 | 154 | Waiting on user... Look at your ledger! 155 | 156 | } 157 | 158 | 159 | 160 | ) 161 | } 162 | 163 | // Default shows error 164 | return ( 165 | Uh oh... something went wrong. You should refresh your browser and start over. 166 | ); 167 | } 168 | 169 | export default WizardLedger 170 | -------------------------------------------------------------------------------- /webserver/src/delegateinfo.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react'; 2 | 3 | import NumberFormat from 'react-number-format'; 4 | 5 | import Col from 'react-bootstrap/Col'; 6 | import Card from 'react-bootstrap/Card'; 7 | import ListGroup from 'react-bootstrap/ListGroup'; 8 | import Loader from "react-loader-spinner"; 9 | 10 | import ToasterContext from './toaster.js'; 11 | import { NO_SIGNER, NOT_REGISTERED, CAN_BAKE, apiRequest } from './util.js'; 12 | 13 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 14 | 15 | 16 | const DelegateInfo = (props) => { 17 | 18 | const { delegate, status } = props; 19 | 20 | let nbDelegators = 0; 21 | const [ balanceInfo, setBalanceInfo ] = useState({ 22 | frozen: 0, 23 | spendable: 0, 24 | total: 0, 25 | stakingBalance: 0, 26 | delegatedBalance: 0, 27 | nbDelegators: 0, 28 | }); 29 | const [ isLoading, setIsLoading ] = useState(false); 30 | const [ connOk, setConnOk ] = useState(true); 31 | const addToast = useContext(ToasterContext); 32 | 33 | useEffect(() => { 34 | 35 | if (status.state === NOT_REGISTERED || status.state === NO_SIGNER) { 36 | return null; 37 | } 38 | 39 | setIsLoading(true); 40 | fetchDelegateInfo(); 41 | 42 | // Update every 5 minutes 43 | let fetchDelegateInfoTimer = setInterval(() => fetchDelegateInfo(), 1000 * 60 * 5); 44 | return () => { 45 | // componentWillUnmount() 46 | clearInterval(fetchDelegateInfoTimer); 47 | fetchDelegateInfoTimer = null; 48 | }; 49 | // eslint-disable-next-line react-hooks/exhaustive-deps 50 | }, []); 51 | 52 | const fetchDelegateInfo = () => { 53 | 54 | // Fetch delegator info which is only necessary when looking at the UI 55 | const apiUrl = "http://"+window.NETWORK+"-us.rpc.bakinbacon.io/chains/main/blocks/head/context/delegates/" + delegate 56 | apiRequest(apiUrl) 57 | .then(data => { 58 | 59 | const balance = parseInt(data.balance, 10); 60 | const frozenBalance = parseInt(data.frozen_balance, 10); 61 | const spendable = balance - frozenBalance; 62 | 63 | // If we loose a delegator, show message 64 | const newNbDelegators = (data.delegated_contracts.length - 1); // Don't count ourselves 65 | 66 | if (nbDelegators > 0 && newNbDelegators > nbDelegators) { 67 | // Gained 68 | addToast({ 69 | title: "New Delegator!", 70 | msg: "You gained a delegator!", 71 | type: "primary", 72 | }); 73 | } else if (nbDelegators > 0 && newNbDelegators < nbDelegators) { 74 | // Lost 75 | addToast({ 76 | title: "Lost Delegator", 77 | msg: "You lost a delegator! No big deal; it happens to everyone.", 78 | type: "info", 79 | }); 80 | } 81 | 82 | // Update variable 83 | nbDelegators = newNbDelegators; 84 | 85 | // Update object for redraw 86 | setBalanceInfo({ 87 | total: (balance / 1e6).toFixed(2), 88 | frozen: (frozenBalance / 1e6).toFixed(2), 89 | spendable: (spendable / 1e6).toFixed(2), 90 | stakingBalance: (parseInt(data.staking_balance, 10) / 1e6).toFixed(2), 91 | delegatedBalance: (parseInt(data.delegated_balance, 10) / 1e6).toFixed(2), 92 | nbDelegators: newNbDelegators, 93 | }); 94 | 95 | }) 96 | .catch((errMsg) => { 97 | console.log(errMsg) 98 | setConnOk(false); 99 | addToast({ 100 | title: "Loading Delegate Error", 101 | msg: errMsg, 102 | type: "danger", 103 | }); 104 | }) 105 | .finally(() => { 106 | setIsLoading(false); 107 | }); 108 | } 109 | 110 | // Returns 111 | if (isLoading || !connOk) { 112 | return ( 113 | <> 114 | 115 |
Loading Baker Info... 116 | 117 | 118 | ) 119 | } 120 | 121 | if (status.state === CAN_BAKE) { 122 | return ( 123 | <> 124 | 125 | 126 | 127 | ) 128 | } 129 | 130 | if (status.state === NOT_REGISTERED || status.state === NO_SIGNER) { 131 | return null; 132 | } 133 | 134 | // Fallback 135 | return (Delegate info currently unavailable.); 136 | } 137 | 138 | const DelegateBalances = (props) => { 139 | 140 | return ( 141 | 142 | Baker Balances 143 | 144 |
Frozen:
{value}
} />
145 |
Spendable:
{value}
} />
146 |
Total:
{value}
} />
147 |
148 |
149 | ) 150 | } 151 | 152 | const DelegateStats = (props) => { 153 | 154 | return ( 155 | 156 | Baker Stats 157 | 158 |
Delegated Balance:
{value}
} />
159 |
Staking Balance:
{value}
} />
160 |
# Delegators:
{value}
} />
161 |
162 |
163 | ) 164 | } 165 | 166 | export default DelegateInfo -------------------------------------------------------------------------------- /webserver/src/wizards/wallet.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import Alert from 'react-bootstrap/Alert' 4 | import Button from 'react-bootstrap/Button'; 5 | import Col from 'react-bootstrap/Col'; 6 | import Card from 'react-bootstrap/Card'; 7 | import Form from 'react-bootstrap/Form' 8 | import Row from 'react-bootstrap/Row'; 9 | 10 | import { apiRequest } from '../util.js'; 11 | 12 | 13 | const WizardWallet = (props) => { 14 | 15 | const { onFinishWizard } = props; 16 | 17 | const [ step, setStep ] = useState(1); 18 | const [ edsk, setEdsk ] = useState(""); 19 | const [ importEdsk, setImportEdsk ] = useState(""); 20 | const [ pkh, setPkh ] = useState(""); 21 | const [ err, setError ] = useState(""); 22 | 23 | const generateNewKey = () => { 24 | const generateKeyApiUrl = window.BASE_URL + "/api/wizard/generateNewKey"; 25 | apiRequest(generateKeyApiUrl) 26 | .then((data) => { 27 | setEdsk(data.edsk); 28 | setPkh(data.pkh); 29 | setStep(2); 30 | }) 31 | .catch((errMsg) => { 32 | console.log(errMsg) 33 | setError(errMsg) 34 | }); 35 | }; 36 | 37 | const exitWizardWallet = () => { 38 | const finishWizardApiUrl = window.BASE_URL + "/api/wizard/finishWallet"; 39 | apiRequest(finishWizardApiUrl) 40 | .then(() => { 41 | // Ignore response body; just need 200 OK 42 | // Call parent finish wizard to exit this sub-wizard 43 | onFinishWizard(); 44 | }) 45 | .catch((errMsg) => { 46 | console.log(errMsg); 47 | setError(errMsg) 48 | }); 49 | } 50 | 51 | const onSecretKeyChange = (e) => { 52 | setImportEdsk(e.target.value); 53 | } 54 | 55 | const doImportKey = () => { 56 | 57 | // Clear previous error messages 58 | setError(""); 59 | 60 | // Sanity checks 61 | if (importEdsk.substring(0, 4) !== "edsk") { 62 | setError("Secret key must begin with 'edsk'"); 63 | return 64 | } 65 | if (importEdsk.length !== 54 && importEdsk.length !== 98) { 66 | setError("Secret key must be 54 or 98 characters long."); 67 | return 68 | } 69 | 70 | // Call API to import key 71 | const importKeyApiUrl = window.BASE_URL + "/api/wizard/importKey"; 72 | const requestOptions = { 73 | method: 'POST', 74 | headers: { 'Content-Type': 'application/json' }, 75 | body: JSON.stringify({ edsk: importEdsk }) 76 | }; 77 | 78 | apiRequest(importKeyApiUrl, requestOptions) 79 | .then((data) => { 80 | setEdsk(data.edsk); 81 | setPkh(data.pkh); 82 | setStep(3); 83 | }) 84 | .catch((errMsg) => { 85 | console.log(errMsg); 86 | setError(errMsg) 87 | }); 88 | } 89 | 90 | // Returns 91 | 92 | // Step 99 is a dummy step that should not ever get rendered 93 | if (step === 99) { return (<>Foo); } 94 | 95 | // This renders inside parent 96 | if (step === 1) { 97 | return( 98 | <> 99 | Setup Software Wallet 100 | 101 | 102 | There are two options when setting up a software wallet: 1) Generate a new secret key, or 2) Import an existing secret key. 103 | Below, make your selection by clicking on 'Generate New Key', or by pasting your existing secret key and clicking 'Import Secret Key'. Your secret key must be unencrypted when importing. 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
OR
114 |
115 | 116 | 117 | 118 | Secret Key 119 | 120 | 121 | 122 | 123 | 124 | 125 | { err && 126 | {err} 127 | } 128 | 129 | ); 130 | } 131 | 132 | // Successfully generated new key; display for user 133 | if (step === 2) { 134 | return ( 135 | <> 136 | Setup Software Wallet 137 | 138 | 139 | Successfully generated new key! 140 | Below you will see your unencrypted secret key, along with your public key hash. 141 | Save a copy of your secret key NOW! It will never be displayed again. Save it somewhere safe. In the future, if you need to restore Bakin'Bacon, you can import this key. 142 | 143 | 144 | 145 | Secret Key: 146 | {edsk} 147 | 148 | 149 | Public Key Hash: 150 | {pkh} 151 | 152 | 153 | 154 | 155 | 156 | ); 157 | } 158 | 159 | // Successfully imported key 160 | if (step === 3) { 161 | return ( 162 | <> 163 | Setup Software Wallet 164 | 165 | 166 | Successfully imported secret key! 167 | Below you will see your public key hash. Confirm this is the correct address. If not, reload this page to try again. 168 | 169 | 170 | 171 | Public Key Hash: 172 | {pkh} 173 | 174 | 175 | 176 | 177 | 178 | ); 179 | } 180 | } 181 | 182 | export default WizardWallet 183 | -------------------------------------------------------------------------------- /webserver/src/settings/notifications.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react'; 2 | 3 | import Button from 'react-bootstrap/Button'; 4 | import Col from 'react-bootstrap/Col'; 5 | import Card from 'react-bootstrap/Card'; 6 | import Form from 'react-bootstrap/Form' 7 | import Row from 'react-bootstrap/Row'; 8 | 9 | import ToasterContext from '../toaster.js'; 10 | import { apiRequest } from '../util.js'; 11 | 12 | 13 | const Notifications = (props) => { 14 | 15 | const { settings, loadSettings } = props; 16 | 17 | const [telegramConfig, setTelegramConfig] = useState(settings.notifications.telegram); 18 | // const [emailConfig, setEmailConfig] = useState(settings.notifications.email); 19 | const addToast = useContext(ToasterContext); 20 | 21 | useEffect(() => { 22 | const config = settings.notifications; 23 | const tConfig = config.telegram; 24 | if (Array.isArray(tConfig.chatids)) { 25 | tConfig.chatids = tConfig.chatids.join(',') 26 | } 27 | if (tConfig.chatids == null) { 28 | tConfig.chatids = "" 29 | } 30 | 31 | if (Object.keys(tConfig).length !== 0) { 32 | setTelegramConfig(tConfig) 33 | } 34 | 35 | // setEmailConfig(config.email) 36 | 37 | }, [settings]); 38 | 39 | const handleTelegramChange = (e) => { 40 | let { name, value } = e.target; 41 | if (name === "enabled") { 42 | value = !telegramConfig.enabled 43 | } 44 | setTelegramConfig((prev) => ({ 45 | ...prev, 46 | [name]: value 47 | })); 48 | } 49 | 50 | // const handleEmailChange = (e) => { 51 | // const { name, value } = e.target; 52 | // setEmailConfig((prev) => ({ 53 | // ...prev, 54 | // [name]: value 55 | // })); 56 | // } 57 | 58 | const saveTelegram = (e) => { 59 | 60 | // Validation first 61 | const chatIds = telegramConfig.chatids.split(/[ ,]/); 62 | for (let i = 0; i < chatIds.length; i++) { 63 | chatIds[i] = Number(chatIds[i]) // Convert strings to int 64 | if (isNaN(chatIds[i])) { 65 | addToast({ 66 | title: "Invalid ChatId", 67 | msg: "Telegram chatId must be a positive or negative number.", 68 | type: "danger", 69 | autohide: 6000, 70 | }); 71 | return; 72 | } 73 | } 74 | 75 | const botapikey = telegramConfig.apikey; 76 | const regex = new RegExp(/\d{9}:[0-9A-Za-z_-]{35}/); 77 | if (!regex.test(botapikey)) { 78 | addToast({ 79 | title: "Invalid Bot API Key", 80 | msg: "Provided API key does not match known pattern.", 81 | type: "danger", 82 | autohide: 6000, 83 | }); 84 | return; 85 | } 86 | 87 | // Validations complete 88 | const apiUrl = window.BASE_URL + "/api/settings/savetelegram" 89 | const postData = { 90 | chatids: chatIds, 91 | apikey: botapikey, 92 | enabled: telegramConfig.enabled, 93 | }; 94 | handlePostAPI(apiUrl, postData).then(() => { 95 | addToast({ 96 | title: "Save Telegram Success", 97 | msg: "Saved Telegram config. You should receive a test message soon. If not, check your config values and save again.", 98 | type: "success", 99 | autohide: 3000, 100 | }); 101 | }) 102 | } 103 | 104 | // Add/Delete RPC, and Save Telegram/Email RPCs use POST and only care if failure. 105 | // On 200 OK, refresh settings 106 | const handlePostAPI = (url, data) => { 107 | 108 | const requestOptions = { 109 | method: 'POST', 110 | headers: { 'Content-Type': 'application/json' }, 111 | body: JSON.stringify(data) 112 | }; 113 | 114 | return apiRequest(url, requestOptions) 115 | .then(() => { 116 | loadSettings(); 117 | }) 118 | .catch((errMsg) => { 119 | console.log(errMsg); 120 | addToast({ 121 | title: "Settings Error", 122 | msg: errMsg, 123 | type: "danger", 124 | }); 125 | }); 126 | } 127 | 128 | return ( 129 | <> 130 | 131 | Notifications 132 | 133 | Bakin'Bacon can send notifications on certain actions: Not enough bond, cannot find ledger, etc. Fill in the required information below to enable different notification destinations. A test message will be sent on 'Save'. 134 | 135 | 136 | 137 | Telegram 138 | 139 | 140 | 141 | Chat Ids 142 | 143 | Separate multiple chatIds with ',' 144 | 145 | 146 | 147 | 148 | Bot API Key 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | Email 168 | 169 | 170 | 171 | COMING SOON! 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | ) 182 | } 183 | 184 | export default Notifications 185 | -------------------------------------------------------------------------------- /webserver/webserver.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "encoding/json" 7 | "fmt" 8 | "html/template" 9 | "io/fs" 10 | "net/http" 11 | "sync" 12 | "time" 13 | 14 | log "github.com/sirupsen/logrus" 15 | 16 | "github.com/gorilla/handlers" 17 | "github.com/gorilla/mux" 18 | "github.com/pkg/errors" 19 | 20 | "bakinbacon/baconclient" 21 | "bakinbacon/notifications" 22 | "bakinbacon/payouts" 23 | "bakinbacon/storage" 24 | ) 25 | 26 | var ( 27 | // Embed all UI objects 28 | //go:embed build 29 | staticUi embed.FS 30 | ) 31 | 32 | type ApiError struct { 33 | Error string `json:"error"` 34 | } 35 | 36 | type TemplateVars struct { 37 | Network string 38 | BlocksPerCycle int 39 | MinBlockTime int 40 | UiBaseUrl string 41 | } 42 | 43 | type WebServer struct { 44 | // Global vars for the webserver package 45 | httpSvr *http.Server 46 | baconClient *baconclient.BaconClient 47 | notificationHandler *notifications.NotificationHandler 48 | payoutsHandler *payouts.PayoutsHandler 49 | storage *storage.Storage 50 | } 51 | 52 | type WebServerArgs struct { 53 | Client *baconclient.BaconClient 54 | NotificationHandler *notifications.NotificationHandler 55 | PayoutsHandler *payouts.PayoutsHandler 56 | Storage *storage.Storage 57 | 58 | BindAddr string 59 | BindPort int 60 | TemplateVars TemplateVars 61 | 62 | ShutdownChannel <-chan interface{} 63 | WG *sync.WaitGroup 64 | } 65 | 66 | 67 | func Start(args WebServerArgs) error { 68 | 69 | if err := args.Validate(); err != nil { 70 | return errors.Wrap(err, "Could not start web server") 71 | } 72 | 73 | ws := &WebServer{ 74 | baconClient: args.Client, 75 | notificationHandler: args.NotificationHandler, 76 | payoutsHandler: args.PayoutsHandler, 77 | storage: args.Storage, 78 | } 79 | 80 | // Repoint web ui down one directory 81 | staticContent, err := fs.Sub(staticUi, "build") 82 | if err != nil { 83 | return errors.Wrap(err, "Could not find UI build directory") 84 | 85 | } 86 | 87 | // index.html 88 | indexTemplate, err := template.ParseFS(staticContent, "index.html") 89 | if err != nil { 90 | return errors.Wrap(err, "Could not parse UI template") 91 | } 92 | 93 | // Set things up 94 | router := mux.NewRouter() 95 | 96 | router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 97 | if err := indexTemplate.Execute(w, args.TemplateVars); err != nil { 98 | log.WithError(err).Error("Unable to render index") 99 | } 100 | }) 101 | 102 | // Root APIs 103 | apiRouter := router.PathPrefix("/api").Subrouter() 104 | apiRouter.HandleFunc("/status", ws.getStatus).Methods("GET") 105 | apiRouter.HandleFunc("/delegate", ws.setDelegate).Methods("POST") 106 | apiRouter.HandleFunc("/health", ws.getHealth).Methods("GET") 107 | 108 | // Settings tab 109 | settingsRouter := apiRouter.PathPrefix("/settings").Subrouter() 110 | settingsRouter.HandleFunc("/", ws.getSettings).Methods("GET") 111 | settingsRouter.HandleFunc("/savetelegram", ws.saveTelegram).Methods("POST") 112 | settingsRouter.HandleFunc("/saveemail", ws.saveEmail).Methods("POST") 113 | settingsRouter.HandleFunc("/addendpoint", ws.addEndpoint).Methods("POST") 114 | settingsRouter.HandleFunc("/listendpoints", ws.listEndpoints).Methods("GET") 115 | settingsRouter.HandleFunc("/deleteendpoint", ws.deleteEndpoint).Methods("POST") 116 | settingsRouter.HandleFunc("/bakersettings", ws.saveBakerSettings).Methods("POST") 117 | 118 | // Payouts tab 119 | payoutsRouter := apiRouter.PathPrefix("/payouts").Subrouter() 120 | payoutsRouter.HandleFunc("/list", ws.getPayouts).Methods("GET") 121 | payoutsRouter.HandleFunc("/cycledetail", ws.getCyclePayouts).Methods("GET") 122 | payoutsRouter.HandleFunc("/sendpayouts", ws.sendCyclePayouts).Methods("POST") 123 | 124 | // Voting tab 125 | votingRouter := apiRouter.PathPrefix("/voting").Subrouter() 126 | votingRouter.HandleFunc("/upvote", ws.handleUpvote).Methods("POST", "OPTIONS") 127 | 128 | // Setup wizards 129 | wizardRouter := apiRouter.PathPrefix("/wizard").Subrouter() 130 | wizardRouter.HandleFunc("/testLedger", ws.testLedger) 131 | wizardRouter.HandleFunc("/confirmBakingPkh", ws.confirmBakingPkh) 132 | wizardRouter.HandleFunc("/generateNewKey", ws.generateNewKey) 133 | wizardRouter.HandleFunc("/importKey", ws.importSecretKey).Methods("POST", "OPTIONS") 134 | wizardRouter.HandleFunc("/registerBaker", ws.registerBaker).Methods("POST", "OPTIONS") 135 | wizardRouter.HandleFunc("/finishWallet", ws.finishWalletWizard) 136 | 137 | // For static content (js, images) 138 | router.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticContent))) 139 | 140 | // Make the http server 141 | httpAddr := fmt.Sprintf("%s:%d", args.BindAddr, args.BindPort) 142 | ws.httpSvr = &http.Server{ 143 | Handler: handlers.CORS( 144 | handlers.AllowedHeaders([]string{"Content-Type"}), 145 | handlers.AllowedOrigins([]string{"*"}), 146 | handlers.AllowedMethods([]string{"GET", "POST", "OPTIONS"}), 147 | )(router), 148 | Addr: httpAddr, 149 | WriteTimeout: 15 * time.Second, 150 | ReadTimeout: 15 * time.Second, 151 | } 152 | 153 | log.WithField("Addr", httpAddr).Info("Bakin'Bacon WebUI Listening") 154 | 155 | // Launch webserver in background 156 | args.WG.Add(1) 157 | go func() { 158 | // TODO: SSL for localhost? 159 | // var err error 160 | // if wantSSL { 161 | // err = httpSvr.ListenAndServeTLS("ssl/cert.pem", "ssl/key.pem") 162 | // } else { 163 | // err = httpSvr.ListenAndServe() 164 | // } 165 | if err := ws.httpSvr.ListenAndServe(); err != nil && err != http.ErrServerClosed { 166 | log.WithError(err).Errorf("Httpserver: ListenAndServe()") 167 | } 168 | 169 | log.Info("Httpserver: Shutdown") 170 | }() 171 | 172 | // Wait for shutdown signal on channel 173 | go func() { 174 | defer args.WG.Done() 175 | <-args.ShutdownChannel 176 | 177 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 178 | defer cancel() 179 | 180 | if err := ws.httpSvr.Shutdown(ctx); err != nil { 181 | log.WithError(err).Errorf("Httpserver: Shutdown()") 182 | } 183 | }() 184 | 185 | return nil 186 | } 187 | 188 | func apiError(err error, w http.ResponseWriter) { 189 | e, _ := json.Marshal(ApiError{err.Error()}) 190 | http.Error(w, string(e), http.StatusBadRequest) 191 | } 192 | 193 | func apiReturnOk(w http.ResponseWriter) { 194 | if err := json.NewEncoder(w).Encode(map[string]string{"ok": "ok"}); err != nil { 195 | log.WithError(err).Error("UI Return Encode Failure") 196 | } 197 | } 198 | 199 | func (a *WebServerArgs) Validate() error { 200 | 201 | if a.Client == nil { 202 | return errors.New("BaconClient is not instantiated") 203 | } 204 | 205 | if a.BindAddr == "" { 206 | return errors.New("Bind address empty") 207 | } 208 | 209 | if a.ShutdownChannel == nil { 210 | return errors.New("Shutdown channel is not instantiated") 211 | } 212 | 213 | if a.WG == nil { 214 | return errors.New("WaitGroup is not instantiated") 215 | } 216 | 217 | return nil 218 | } 219 | -------------------------------------------------------------------------------- /nonce.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/hex" 7 | "encoding/json" 8 | "regexp" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/bakingbacon/go-tezos/v4/crypto" 13 | "github.com/bakingbacon/go-tezos/v4/forge" 14 | "github.com/bakingbacon/go-tezos/v4/rpc" 15 | 16 | log "github.com/sirupsen/logrus" 17 | 18 | "bakinbacon/nonce" 19 | "bakinbacon/util" 20 | ) 21 | 22 | var previouslyInjectedErr = regexp.MustCompile(`while applying operation (o[a-zA-Z0-9]{50}).*previously revealed`) 23 | 24 | func (bb *BakinBacon) generateNonce() (nonce.Nonce, error) { 25 | 26 | // Generate a 64 char hexadecimal seed from random 32 bytes 27 | randBytes := make([]byte, 32) 28 | if _, err := rand.Read(randBytes); err != nil { 29 | log.WithError(err).Error("Unable to read random bytes") 30 | return nonce.Nonce{}, err 31 | } 32 | 33 | nonceHash, err := util.CryptoGenericHash(randBytes, []byte{}) 34 | if err != nil { 35 | log.WithError(err).Error("Unable to hash rand bytes for nonce") 36 | return nonce.Nonce{}, err 37 | } 38 | 39 | // B58 encode seed hash with nonce prefix 40 | encodedNonce := crypto.B58cencode(nonceHash, nonce.Prefix_nonce) 41 | 42 | n := nonce.Nonce{ 43 | Seed: hex.EncodeToString(randBytes), 44 | Nonce: nonceHash, 45 | EncodedNonce: encodedNonce, 46 | NoPrefixNonce: hex.EncodeToString(nonceHash), 47 | } 48 | 49 | return n, nil 50 | } 51 | 52 | func (bb *BakinBacon) revealNonces(ctx context.Context, wg *sync.WaitGroup, block rpc.Block) { 53 | 54 | // Decrement waitGroup on exit 55 | defer wg.Done() 56 | 57 | // Handle panic gracefully 58 | defer func() { 59 | if r := recover(); r != nil { 60 | log.WithField("Message", r).Error("Panic recovered in revealNonces") 61 | } 62 | }() 63 | 64 | // Only reveal in levels 1-16 of cycle 65 | cyclePosition := block.Metadata.Level.CyclePosition 66 | if cyclePosition == 0 || cyclePosition > 256 { 67 | return 68 | } 69 | 70 | // Get nonces for previous cycle from DB 71 | previousCycle := block.Metadata.Level.Cycle - 1 72 | 73 | // Returns []json.RawMessage... 74 | noncesRawBytes, err := bb.GetNoncesForCycle(previousCycle) 75 | if err != nil { 76 | log.WithError(err).WithField("Cycle", previousCycle).Warn("Unable to get nonces from DB") 77 | return 78 | } 79 | 80 | // ...need to unmarshal 81 | unrevealedNonces := make([]nonce.Nonce, 0) 82 | for _, b := range noncesRawBytes { 83 | 84 | var tmpNonce nonce.Nonce 85 | if err := json.Unmarshal(b, &tmpNonce); err != nil { 86 | log.WithError(err).Error("Unable to unmarshal nonce") 87 | continue 88 | } 89 | 90 | // Filter out nonces which have been revealed 91 | if tmpNonce.RevealOp != "" { 92 | log.WithFields(log.Fields{ 93 | "Level": tmpNonce.Level, "RevealedOp": tmpNonce.RevealOp, 94 | }).Debug("Nonce already revealed") 95 | 96 | continue 97 | } 98 | 99 | unrevealedNonces = append(unrevealedNonces, tmpNonce) 100 | } 101 | 102 | // Any unrevealed nonces? 103 | if len(unrevealedNonces) == 0 { 104 | log.WithField("Cycle", previousCycle).Info("No nonces to reveal") 105 | return 106 | } 107 | 108 | log.WithField("Cycle", previousCycle).Infof("Found %d unrevealed nonces", len(unrevealedNonces)) 109 | 110 | hashBlockID := rpc.BlockIDHash(block.Hash) 111 | 112 | // loop over unrevealed nonces and inject 113 | for _, nonce := range unrevealedNonces { 114 | 115 | log.WithFields(log.Fields{ 116 | "Level": nonce.Level, "Nonce": nonce.EncodedNonce, "Seed": nonce.Seed, 117 | }).Info("Revealing nonce") 118 | 119 | nonceRevelation := rpc.Content{ 120 | Kind: rpc.SEEDNONCEREVELATION, 121 | Level: nonce.Level, 122 | Nonce: nonce.Seed, 123 | } 124 | 125 | nonceRevelationBytes, err := forge.Encode(block.Hash, nonceRevelation) 126 | if err != nil { 127 | log.WithError(err).Error("Error Forging Nonce Reveal") 128 | 129 | return 130 | } 131 | nonceRevelationBytes += strings.Repeat("0", 128) // Nonce requires a null signature 132 | 133 | log.WithField("Bytes", nonceRevelationBytes).Trace("Forged Nonce Reveal") 134 | 135 | // Build preapply using null signature 136 | preapplyNonceRevealOp := rpc.PreapplyOperationsInput{ 137 | BlockID: &hashBlockID, 138 | Operations: []rpc.Operations{ 139 | { 140 | Protocol: block.Protocol, 141 | Branch: block.Hash, 142 | Contents: rpc.Contents{ 143 | nonceRevelation, 144 | }, 145 | Signature: "edsigtXomBKi5CTRf5cjATJWSyaRvhfYNHqSUGrn4SdbYRcGwQrUGjzEfQDTuqHhuA8b2d8NarZjz8TRf65WkpQmo423BtomS8Q", 146 | }, 147 | }, 148 | } 149 | 150 | // Validate the operation against the node for any errors 151 | resp, preApplyResp, err := bb.Current.PreapplyOperations(preapplyNonceRevealOp) 152 | if err != nil { 153 | 154 | // If somehow the nonce reveal was already injected, but we have no record of the opHash, 155 | // we can inject it again without worry to discover the opHash and save it 156 | if strings.Contains(resp.String(), "nonce.previously_revealed") { 157 | 158 | log.Warn("Nonce previously injected, unknown opHash.") 159 | 160 | } else { 161 | 162 | // Any other error we display and move to next nonce 163 | log.WithError(err).WithFields(log.Fields{ 164 | "Request": resp.Request.URL, "Response": string(resp.Body()), 165 | }).Error("Could not preapply nonce reveal operation") 166 | 167 | continue 168 | } 169 | 170 | } else { 171 | 172 | // Preapply success 173 | log.WithField("Response", preApplyResp).Trace("Nonce Preapply") 174 | log.Info("Nonce Preapply Successful") 175 | } 176 | 177 | // Check if new block came in 178 | select { 179 | case <-ctx.Done(): 180 | log.Warn("New block arrived; Canceling nonce reveal") 181 | return 182 | default: 183 | // No need to wait 184 | break 185 | } 186 | 187 | // Inject nonce reveal op 188 | injectionInput := rpc.InjectionOperationInput{ 189 | Operation: nonceRevelationBytes, 190 | } 191 | 192 | resp, revealOpHash, err := bb.Current.InjectionOperation(injectionInput) 193 | if err != nil { 194 | 195 | // Check error message for possible previous injection. If notice not present 196 | // then we have a real error on our hands. If notice present, let func finish 197 | // and save operational hash to DB 198 | parts := previouslyInjectedErr.FindStringSubmatch(resp.String()) 199 | if len(parts) > 0 { 200 | revealOpHash = parts[1] 201 | } else { 202 | 203 | log.WithError(err).WithFields(log.Fields{ 204 | "Response": resp.String(), 205 | }).Error("Error Injecting Nonce Reveal") 206 | 207 | continue 208 | } 209 | } 210 | 211 | log.WithField("OperationHash", revealOpHash).Info("Nonce Reveal Injected") 212 | 213 | // Update DB with hash of reveal operation 214 | nonce.RevealOp = revealOpHash 215 | 216 | // Marshal for DB 217 | nonceBytes, err := json.Marshal(nonce) 218 | if err != nil { 219 | log.WithError(err).Error("Unable to marshal nonce") 220 | continue 221 | } 222 | 223 | if err := bb.SaveNonce(previousCycle, nonce.Level, nonceBytes); err != nil { 224 | log.WithError(err).Error("Unable to save nonce reveal to DB") 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /storage/config.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | 7 | "github.com/pkg/errors" 8 | 9 | bolt "go.etcd.io/bbolt" 10 | 11 | "bakinbacon/util" 12 | ) 13 | 14 | const ( 15 | PUBLIC_KEY_HASH = "pkh" 16 | BIP_PATH = "bippath" 17 | SIGNER_TYPE = "signertype" 18 | SIGNER_SK = "signersk" 19 | BAKER_FEE = "bakerfee" 20 | UI_EXPLORER = "uiexplorer" 21 | ) 22 | 23 | func (s *Storage) GetBakerSettings() (map[string]interface{}, error) { 24 | 25 | settings := make(map[string]interface{}) 26 | 27 | err := s.View(func(tx *bolt.Tx) error { 28 | b := tx.Bucket([]byte(CONFIG_BUCKET)) 29 | 30 | settings[BAKER_FEE] = strconv.Itoa(Btoi(b.Get([]byte(BAKER_FEE)))) 31 | settings[UI_EXPLORER] = string(b.Get([]byte(UI_EXPLORER))) 32 | 33 | return nil 34 | }) 35 | 36 | return settings, err 37 | } 38 | 39 | func (s *Storage) SaveBakerSettings(settings map[string]string) error { 40 | 41 | bakerFee, err := strconv.Atoi(settings[BAKER_FEE]) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return s.Update(func(tx *bolt.Tx) error { 47 | b := tx.Bucket([]byte(CONFIG_BUCKET)) 48 | 49 | if err := b.Put([]byte(BAKER_FEE), Itob(bakerFee)); err != nil { 50 | return err 51 | } 52 | 53 | if err := b.Put([]byte(UI_EXPLORER), []byte(settings[UI_EXPLORER])); err != nil { 54 | return err 55 | } 56 | 57 | return nil 58 | }) 59 | } 60 | 61 | func (s *Storage) GetDelegate() (string, string, error) { 62 | 63 | var sk, pkh string 64 | 65 | err := s.View(func(tx *bolt.Tx) error { 66 | b := tx.Bucket([]byte(CONFIG_BUCKET)) 67 | sk = string(b.Get([]byte(SIGNER_SK))) 68 | pkh = string(b.Get([]byte(PUBLIC_KEY_HASH))) 69 | 70 | return nil 71 | }) 72 | 73 | return sk, pkh, err 74 | } 75 | 76 | func (s *Storage) SetDelegate(sk, pkh string) error { 77 | 78 | return s.Update(func(tx *bolt.Tx) error { 79 | b := tx.Bucket([]byte(CONFIG_BUCKET)) 80 | 81 | if err := b.Put([]byte(SIGNER_SK), []byte(sk)); err != nil { 82 | return err 83 | } 84 | 85 | if err := b.Put([]byte(PUBLIC_KEY_HASH), []byte(pkh)); err != nil { 86 | return err 87 | } 88 | 89 | return nil 90 | }) 91 | } 92 | 93 | func (s *Storage) GetSignerType() (int, error) { 94 | 95 | var signerType int = 0 96 | 97 | err := s.View(func(tx *bolt.Tx) error { 98 | b := tx.Bucket([]byte(CONFIG_BUCKET)) 99 | signerTypeBytes := b.Get([]byte(SIGNER_TYPE)) 100 | if signerTypeBytes != nil { 101 | signerType = Btoi(signerTypeBytes) 102 | } 103 | 104 | return nil 105 | }) 106 | 107 | return signerType, err 108 | } 109 | 110 | func (s *Storage) SetSignerType(signerType int) error { 111 | 112 | return s.Update(func(tx *bolt.Tx) error { 113 | b := tx.Bucket([]byte(CONFIG_BUCKET)) 114 | return b.Put([]byte(SIGNER_TYPE), Itob(signerType)) 115 | }) 116 | } 117 | 118 | func (s *Storage) GetSignerSk() (string, error) { 119 | 120 | var sk string 121 | 122 | err := s.View(func(tx *bolt.Tx) error { 123 | b := tx.Bucket([]byte(CONFIG_BUCKET)) 124 | sk = string(b.Get([]byte(SIGNER_SK))) 125 | return nil 126 | }) 127 | 128 | return sk, err 129 | } 130 | 131 | func (s *Storage) SetSignerSk(sk string) error { 132 | 133 | return s.Update(func(tx *bolt.Tx) error { 134 | b := tx.Bucket([]byte(CONFIG_BUCKET)) 135 | return b.Put([]byte(SIGNER_SK), []byte(sk)) 136 | }) 137 | } 138 | 139 | // Ledger 140 | func (s *Storage) SaveLedgerToDB(pkh, bipPath string, ledgerType int) error { 141 | 142 | return s.Update(func(tx *bolt.Tx) error { 143 | 144 | b := tx.Bucket([]byte(CONFIG_BUCKET)) 145 | 146 | // Save signer type as ledger 147 | if err := b.Put([]byte(SIGNER_TYPE), Itob(ledgerType)); err != nil { 148 | return err 149 | } 150 | 151 | // Save PKH 152 | if err := b.Put([]byte(PUBLIC_KEY_HASH), []byte(pkh)); err != nil { 153 | return err 154 | } 155 | 156 | // Save BipPath 157 | if err := b.Put([]byte(BIP_PATH), []byte(bipPath)); err != nil { 158 | return err 159 | } 160 | 161 | return nil 162 | }) 163 | } 164 | 165 | func (s *Storage) GetLedgerConfig() (string, string, error) { 166 | 167 | var pkh, bipPath string 168 | 169 | err := s.View(func(tx *bolt.Tx) error { 170 | b := tx.Bucket([]byte(CONFIG_BUCKET)) 171 | pkh = string(b.Get([]byte(PUBLIC_KEY_HASH))) 172 | bipPath = string(b.Get([]byte(BIP_PATH))) 173 | return nil 174 | }) 175 | 176 | return pkh, bipPath, err 177 | } 178 | 179 | func (s *Storage) AddRPCEndpoint(endpoint string) (int, error) { 180 | 181 | var rpcId int = 0 182 | 183 | err := s.Update(func(tx *bolt.Tx) error { 184 | b := tx.Bucket([]byte(CONFIG_BUCKET)).Bucket([]byte(ENDPOINTS_BUCKET)) 185 | if b == nil { 186 | return errors.New("AddRPC - Unable to locate endpoints bucket") 187 | } 188 | 189 | var foundDup bool 190 | endpointBytes := []byte(endpoint) 191 | 192 | if err := b.ForEach(func(k, v []byte) error { 193 | if bytes.Equal(v, endpointBytes) { 194 | foundDup = true 195 | } 196 | return nil 197 | }); err != nil { 198 | return err 199 | } 200 | 201 | if foundDup { 202 | // Found duplicate, exit 203 | return nil 204 | } 205 | 206 | // else, add 207 | id, _ := b.NextSequence() 208 | rpcId = int(id) 209 | 210 | return b.Put(Itob(int(id)), endpointBytes) 211 | }) 212 | 213 | return rpcId, err 214 | } 215 | 216 | func (s *Storage) GetRPCEndpoints() (map[int]string, error) { 217 | 218 | endpoints := make(map[int]string) 219 | 220 | err := s.View(func(tx *bolt.Tx) error { 221 | b := tx.Bucket([]byte(CONFIG_BUCKET)).Bucket([]byte(ENDPOINTS_BUCKET)) 222 | if b == nil { 223 | return errors.New("GetRPC - Unable to locate endpoints bucket") 224 | } 225 | 226 | if err := b.ForEach(func(k, v []byte) error { 227 | id := Btoi(k) 228 | endpoints[id] = string(v) 229 | 230 | return nil 231 | }); err != nil { 232 | return err 233 | } 234 | 235 | return nil 236 | }) 237 | 238 | return endpoints, err 239 | } 240 | 241 | func (s *Storage) DeleteRPCEndpoint(endpointId int) error { 242 | 243 | return s.Update(func(tx *bolt.Tx) error { 244 | b := tx.Bucket([]byte(CONFIG_BUCKET)).Bucket([]byte(ENDPOINTS_BUCKET)) 245 | if b == nil { 246 | return errors.New("Unable to locate endpoints bucket") 247 | } 248 | 249 | return b.Delete(Itob(endpointId)) 250 | }) 251 | } 252 | 253 | func (s *Storage) AddDefaultEndpoints(network string) error { 254 | 255 | // Check the current sequence id for endpoints bucket. If > 2, then 256 | // this is not a first-time init and we should not add these again 257 | 258 | var currentSeq uint64 259 | 260 | err := s.View(func(tx *bolt.Tx) error { 261 | b := tx.Bucket([]byte(CONFIG_BUCKET)).Bucket([]byte(ENDPOINTS_BUCKET)) 262 | if b == nil { 263 | return errors.New("AddDefaultRPCs - Unable to locate endpoints bucket") 264 | } 265 | currentSeq = b.Sequence() 266 | 267 | return nil 268 | }) 269 | if err != nil { 270 | return err 271 | } 272 | 273 | if currentSeq == 0 { 274 | 275 | // Statically add BakinBacon's RPC endpoints 276 | switch network { 277 | case util.NETWORK_MAINNET: 278 | _, _ = s.AddRPCEndpoint("http://mainnet-us.rpc.bakinbacon.io") 279 | _, _ = s.AddRPCEndpoint("http://mainnet-eu.rpc.bakinbacon.io") 280 | 281 | case util.NETWORK_GRANADANET: 282 | _, _ = s.AddRPCEndpoint("http://granadanet-us.rpc.bakinbacon.io") 283 | _, _ = s.AddRPCEndpoint("http://granadanet-eu.rpc.bakinbacon.io") 284 | 285 | case util.NETWORK_HANGZHOUNET: 286 | _, _ = s.AddRPCEndpoint("http://hangzhounet-us.rpc.bakinbacon.io") 287 | 288 | default: 289 | return errors.New("Unknown network for storage") 290 | } 291 | } 292 | 293 | return nil 294 | } 295 | -------------------------------------------------------------------------------- /endorsing.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/pkg/errors" 11 | 12 | "github.com/bakingbacon/go-tezos/v4/forge" 13 | "github.com/bakingbacon/go-tezos/v4/rpc" 14 | 15 | log "github.com/sirupsen/logrus" 16 | 17 | "bakinbacon/notifications" 18 | ) 19 | 20 | /* 21 | $ ~/.opam/for_tezos/bin/tezos-codec decode 009-PsFLoren.operation from 5b3c0553c157d641f205f97c6fa480c98b156a75ca2db43e2a202c2460b689f90a000000655b3c0553c157d641f205f97c6fa480c98b156a75ca2db43e2a202c2460b689f9000002392a9b99b4c1f735fb26bc376703ef3ab6b3bf69e07aab1dd09596ac7f196c9a365dbf384d88147aef2c697577596176c6991f46dcd9eb43752ce9632774e2c26008000900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 22 | { "branch": "BLQToCX7mDU2bVuDQXcNAD4FixVNxBA8CBEpcmSQBiPRfgAk6Mc", 23 | "contents": 24 | [ { "kind": "endorsement_with_slot", 25 | "endorsement": 26 | { "branch": "BLQToCX7mDU2bVuDQXcNAD4FixVNxBA8CBEpcmSQBiPRfgAk6Mc", 27 | "operations": { "kind": "endorsement", "level": 145706 }, 28 | "signature": 29 | "sigiLztJohJDzskahhQ2YAfcjRrZjRE8GuxTZK3B3bLdEtfQtHyeQ7VWbBYwiquYg4yU5CDPyGgW4Fecd54q9NiVVWHurdtR" }, 30 | "slot": 9 } ], 31 | "signature": 32 | "edsigtXomBKi5CTRf5cjATJWSyaRvhfYNHqSUGrn4SdbYRcGwQrUGjzEfQDTuqHhuA8b2d8NarZjz8TRf65WkpQmo423BtomS8Q" } 33 | */ 34 | 35 | func (bb *BakinBacon) handleEndorsement(ctx context.Context, wg *sync.WaitGroup, block rpc.Block) { 36 | 37 | // Decrement waitGroup on exit 38 | defer wg.Done() 39 | 40 | // Handle panic gracefully 41 | defer func() { 42 | if r := recover(); r != nil { 43 | log.WithField("Message", r).Error("Panic recovered in handleEndorsement") 44 | } 45 | }() 46 | 47 | endorsingLevel := block.Header.Level 48 | 49 | // Check watermark to ensure we have not endorsed at this level before 50 | watermark, err := bb.GetEndorsingWatermark() 51 | if err != nil { 52 | // watermark = 0 on DB error 53 | log.WithError(err).Error("Unable to get endorsing watermark from DB") 54 | } 55 | 56 | if watermark >= endorsingLevel { 57 | log.WithFields(log.Fields{ 58 | "EndorsingLevel": endorsingLevel, "Watermark": watermark, 59 | }).Error("Watermark level higher than endorsing level; Canceling to prevent double endorsing") 60 | 61 | return 62 | } 63 | 64 | // look for endorsing rights at this level 65 | hashBlockID := rpc.BlockIDHash(block.Hash) 66 | endorsingRightsFilter := rpc.EndorsingRightsInput{ 67 | BlockID: &hashBlockID, 68 | Level: endorsingLevel, 69 | Delegate: bb.Signer.BakerPkh, 70 | } 71 | 72 | resp, endorsingRights, err := bb.Current.EndorsingRights(endorsingRightsFilter) 73 | 74 | log.WithFields(log.Fields{ 75 | "Level": endorsingLevel, "Request": resp.Request.URL, "Response": string(resp.Body()), 76 | }).Trace("Fetching endorsing rights") 77 | 78 | if err != nil { 79 | log.WithError(err).Error("Unable to fetch endorsing rights") 80 | return 81 | } 82 | 83 | if len(endorsingRights) == 0 { 84 | log.WithField("Level", endorsingLevel).Info("No endorsing rights for this level") 85 | return 86 | } 87 | 88 | // Check for new block 89 | select { 90 | case <-ctx.Done(): 91 | log.Warn("New block arrived; Canceling endorsement") 92 | return 93 | default: 94 | break 95 | } 96 | 97 | // Join up all endorsing slots for sorting 98 | var allSlots []int 99 | for _, e := range endorsingRights { 100 | allSlots = append(allSlots, e.Slots...) 101 | } 102 | 103 | // 009 requires the lowest slot be submitted 104 | sort.Ints(allSlots) 105 | 106 | slotString := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(allSlots)), ","), "[]") 107 | log.WithFields(log.Fields{ 108 | "Level": endorsingLevel, "Slots": slotString, 109 | }).Info("Endorsing rights found") 110 | 111 | // Continue since we have at least 1 endorsing right 112 | // Check if we can pay bond 113 | requiredBond := bb.NetworkConstants.EndorsementSecurityDeposit 114 | 115 | if spendableBalance, err := bb.GetSpendableBalance(); err != nil { 116 | log.WithError(err).Error("Unable to get spendable balance") 117 | 118 | // Even if error here, we can still proceed. 119 | // Might have enough to post bond, might not. 120 | 121 | } else if requiredBond > spendableBalance { 122 | 123 | // If not enough bond, exit early 124 | 125 | msg := "Bond balance too low for endorsing" 126 | log.WithFields(log.Fields{ 127 | "Spendable": spendableBalance, "ReqBond": requiredBond, 128 | }).Error(msg) 129 | 130 | bb.Status.SetError(errors.New(msg)) 131 | bb.SendNotification(msg, notifications.BALANCE) 132 | 133 | return 134 | } 135 | 136 | // Continue; have rights, have enough bond 137 | 138 | // Inner endorsement; forge and sign 139 | endorsementContent := rpc.Content{ 140 | Kind: rpc.ENDORSEMENT, 141 | Level: endorsingLevel, 142 | } 143 | 144 | // Inner endorsement bytes 145 | endorsementBytes, err := forge.Encode(block.Hash, endorsementContent) 146 | if err != nil { 147 | log.WithError(err).Error("Error Forging Inner Endorsement") 148 | return 149 | } 150 | 151 | log.WithField("Bytes", endorsementBytes).Debug("Forged Inlined Endorsement") 152 | 153 | // sign inner endorsement 154 | signedInnerEndorsement, err := bb.Signer.SignEndorsement(endorsementBytes, block.ChainID) 155 | if err != nil { 156 | log.WithError(err).Error("Signer endorsement failure") 157 | return 158 | } 159 | 160 | // Outer endorsement 161 | endorseWithSlot := rpc.Content{ 162 | Kind: rpc.ENDORSEMENT_WITH_SLOT, 163 | Endorsement: &rpc.InlinedEndorsement{ 164 | Branch: block.Hash, 165 | Operations: &rpc.InlinedEndorsementOperations{ 166 | Kind: rpc.ENDORSEMENT, 167 | Level: endorsingLevel, 168 | }, 169 | Signature: signedInnerEndorsement.EDSig, 170 | }, 171 | Slot: allSlots[0], 172 | } 173 | 174 | // Outer bytes 175 | endorseWithSlotBytes, err := forge.Encode(block.Hash, endorseWithSlot) 176 | if err != nil { 177 | log.WithError(err).Error("Error Forging Outer Endorsement") 178 | return 179 | } 180 | 181 | // Really low-level debugging 182 | // log.WithField("SignedOp", signedInnerEndorsement.SignedOperation).Debug("SIGNED OP") 183 | // log.WithField("DecodedSig", signedInnerEndorsement.Signature).Debug("DECODED SIG") 184 | // log.WithField("Signature", signedInnerEndorsement.EDSig).Debug("EDSIG") 185 | 186 | // Create injection 187 | injectionInput := rpc.InjectionOperationInput{ 188 | Operation: endorseWithSlotBytes, 189 | } 190 | 191 | // Check if a new block has been posted to /head and we should abort 192 | select { 193 | case <-ctx.Done(): 194 | log.Warn("New block arrived; Canceling endorsement") 195 | return 196 | default: 197 | break 198 | } 199 | 200 | // Dry-run check 201 | if bb.dryRunEndorsement { 202 | log.Warn("Not Injecting Endorsement; Dry-Run Mode") 203 | return 204 | } 205 | 206 | // Inject endorsement 207 | resp, opHash, err := bb.Current.InjectionOperation(injectionInput) 208 | if err != nil { 209 | log.WithError(err).WithFields(log.Fields{ 210 | "Request": resp.Request.URL, "Response": string(resp.Body()), 211 | }).Error("Endorsement Injection Failure") 212 | 213 | return 214 | } 215 | 216 | log.WithField("Operation", opHash).Info("Endorsement Injected") 217 | 218 | // Save endorsement to DB for watermarking 219 | if err := bb.RecordEndorsement(endorsingLevel, opHash); err != nil { 220 | log.WithError(err).Error("Unable to save endorsement; Watermark compromised") 221 | } 222 | 223 | // Update status for UI 224 | bb.Status.SetRecentEndorsement(endorsingLevel, block.Metadata.Level.Cycle, opHash) 225 | } 226 | -------------------------------------------------------------------------------- /baconsigner/ledger.go: -------------------------------------------------------------------------------- 1 | package baconsigner 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/pkg/errors" 9 | 10 | ledger "github.com/bakingbacon/goledger/ledger-apps/tezos" 11 | log "github.com/sirupsen/logrus" 12 | 13 | "bakinbacon/storage" 14 | ) 15 | 16 | const ( 17 | DEFAULT_BIP_PATH = "/44'/1729'/0'/0'" 18 | ) 19 | 20 | type LedgerInfo struct { 21 | Version string `json:"version"` 22 | PrevAuth bool `json:"prevAuth"` 23 | Pkh string `json:"pkh"` 24 | BipPath string `json:"bipPath"` 25 | } 26 | 27 | type LedgerSigner struct { 28 | Info *LedgerInfo 29 | 30 | // Actual object of the ledger 31 | ledger *ledger.TezosLedger 32 | storage *storage.Storage 33 | lock sync.Mutex 34 | } 35 | 36 | var L *LedgerSigner 37 | 38 | func InitLedgerSigner(db *storage.Storage) error { 39 | 40 | L = &LedgerSigner{ 41 | Info: &LedgerInfo{}, 42 | storage: db, 43 | } 44 | 45 | // Get device 46 | dev, err := ledger.Get() 47 | if err != nil { 48 | return errors.Wrap(err, "Cannot get ledger device") 49 | } 50 | 51 | L.ledger = dev 52 | 53 | // Get bipPath and PKH from DB 54 | pkh, dbBipPath, err := L.storage.GetLedgerConfig() 55 | if err != nil { 56 | return errors.Wrap(err, "Cannot load ledger config from DB") 57 | } 58 | 59 | // Sanity 60 | if dbBipPath == "" { 61 | return errors.New("No BIP path found in DB. Cannot configure ledger.") 62 | } 63 | 64 | // Sanity check if wallet app is open instead of baking app 65 | if _, err := L.IsBakingApp(); err != nil { 66 | return err 67 | } 68 | 69 | // Get the bipPath that is authorized to bake 70 | authBipPath, err := L.GetAuthorizedKeyPath() 71 | if err != nil { 72 | return errors.Wrap(err, "Cannot get auth BIP path from ledger") 73 | } 74 | 75 | // Compare to DB config for sanity 76 | if dbBipPath != authBipPath { 77 | return errors.New(fmt.Sprintf("Authorized BipPath, %s, does not match DB Config, %s", authBipPath, dbBipPath)) 78 | } 79 | 80 | // Set dbBipPath from DB config 81 | if err := L.SetBipPath(dbBipPath); err != nil { 82 | return errors.Wrap(err, "Cannot set BIP path on ledger device") 83 | } 84 | 85 | // Get the pkh from dbBipPath from DB config 86 | _, compPkh, err := L.GetPublicKey() 87 | if err != nil { 88 | return errors.Wrap(err, "Cannot fetch pkh from ledger") 89 | } 90 | 91 | if pkh != compPkh { 92 | return errors.New(fmt.Sprintf("Authorized PKH, %s, does not match DB Config, %s", compPkh, pkh)) 93 | } 94 | 95 | L.Info.Pkh = pkh 96 | L.Info.BipPath = authBipPath 97 | 98 | log.WithFields(log.Fields{"KeyPath": authBipPath, "PKH": pkh}).Debug("Ledger Baking Config") 99 | 100 | return nil 101 | } 102 | 103 | func (s *LedgerSigner) Close() { 104 | 105 | s.lock.Lock() 106 | defer s.lock.Unlock() 107 | 108 | s.ledger.Close() 109 | } 110 | 111 | // GetPublicKey Gets the public key from ledger device 112 | func (s *LedgerSigner) GetPublicKey() (string, string, error) { 113 | 114 | s.lock.Lock() 115 | defer s.lock.Unlock() 116 | 117 | // ledger.GetPublicKey returns (pk, pkh, error) 118 | pk, pkh, err := s.ledger.GetPublicKey() 119 | 120 | return pk, pkh, err 121 | } 122 | 123 | func (s *LedgerSigner) SignBytes(opBytes []byte) (string, error) { 124 | 125 | s.lock.Lock() 126 | defer s.lock.Unlock() 127 | 128 | // Returns b58 encoded signature 129 | return s.ledger.SignBytes(opBytes) 130 | } 131 | 132 | func (s *LedgerSigner) IsBakingApp() (string, error) { 133 | 134 | s.lock.Lock() 135 | defer s.lock.Unlock() 136 | 137 | version, err := s.ledger.GetVersion() 138 | if err != nil { 139 | log.WithError(err).Error("Unable to GetVersion") 140 | return "", errors.Wrap(err, "Unable to get app version") 141 | } 142 | 143 | // Check if baking or wallet app is open 144 | if strings.HasPrefix(version, "Wallet") { 145 | return "", errors.New("The Tezos Wallet app is currently open. Please close it and open the Tezos Baking app.") 146 | } 147 | 148 | return version, nil 149 | } 150 | 151 | func (s *LedgerSigner) GetAuthorizedKeyPath() (string, error) { 152 | 153 | s.lock.Lock() 154 | defer s.lock.Unlock() 155 | 156 | return s.ledger.GetAuthorizedKeyPath() 157 | } 158 | 159 | func (s *LedgerSigner) SetBipPath(p string) error { 160 | 161 | s.lock.Lock() 162 | defer s.lock.Unlock() 163 | 164 | return s.ledger.SetBipPath(p) 165 | } 166 | 167 | // TestLedger This function is only called from web UI during initial setup. 168 | // It will open the ledger, get the version string of the running app, and 169 | // fetch either the currently auth'd baking key, or fetch the default BIP path key 170 | func TestLedger(db *storage.Storage) (*LedgerInfo, error) { 171 | 172 | L = &LedgerSigner{ 173 | Info: &LedgerInfo{}, 174 | storage: db, 175 | } 176 | 177 | // Get device 178 | dev, err := ledger.Get() 179 | if err != nil { 180 | return L.Info, errors.Wrap(err, "Cannot get ledger device") 181 | } 182 | L.ledger = dev 183 | 184 | version, err := L.IsBakingApp() 185 | if err != nil { 186 | return L.Info, err 187 | } 188 | 189 | L.Info.Version = version 190 | log.WithField("Version", L.Info.Version).Info("Ledger Version") 191 | 192 | // Check if ledger is already configured for baking 193 | L.Info.BipPath = DEFAULT_BIP_PATH 194 | 195 | bipPath, err := L.GetAuthorizedKeyPath() 196 | if err != nil { 197 | log.WithError(err).Error("Unable to GetAuthorizedKeyPath") 198 | return L.Info, errors.Wrap(err, "Unable to query auth path") 199 | } 200 | 201 | // Check returned path from device 202 | if bipPath != "" { 203 | // Ledger is already setup for baking 204 | log.WithField("Path", bipPath).Info("Ledger previously configured for baking") 205 | L.Info.PrevAuth = true 206 | L.Info.BipPath = bipPath 207 | } 208 | 209 | // Get the key from the path 210 | if err := L.SetBipPath(L.Info.BipPath); err != nil { 211 | log.WithError(err).Error("Unable to SetBipPath") 212 | return L.Info, errors.Wrap(err, "Unable to set bip path") 213 | } 214 | 215 | _, pkh, err := L.GetPublicKey() 216 | if err != nil { 217 | log.WithError(err).Error("Unable to GetPublicKey") 218 | return L.Info, err 219 | } 220 | 221 | L.Info.Pkh = pkh 222 | 223 | return L.Info, nil 224 | } 225 | 226 | // 227 | // ConfirmBakingPkh Ask ledger to display request for public key. User must press button to confirm. 228 | func (s *LedgerSigner) ConfirmBakingPkh(pkh, bipPath string) error { 229 | 230 | log.WithFields(log.Fields{ 231 | "PKH": pkh, "Path": bipPath, 232 | }).Debug("Confirming Baking PKH") 233 | 234 | // Get the key from the path 235 | if err := s.SetBipPath(bipPath); err != nil { 236 | log.WithError(err).Error("Unable to SetBipPath") 237 | return errors.Wrap(err, "Unable to set bip path") 238 | } 239 | 240 | // Ask user to confirm PKH and authorize for baking 241 | _, authPkh, err := s.ledger.AuthorizeBaking() 242 | if err != nil { 243 | log.WithError(err).Error("Unable to AuthorizeBaking") 244 | return errors.Wrap(err, "Unable to authorize baking on device") 245 | } 246 | 247 | // Minor sanity check 248 | if pkh != authPkh { 249 | log.WithFields(log.Fields{ 250 | "AuthPKH": authPkh, "PKH": pkh, 251 | }).Error("PKH and authorized PKH do not match.") 252 | return errors.New("PKH and authorized PKH do not match.") 253 | } 254 | 255 | // Save config to DB 256 | if err := s.storage.SaveLedgerToDB(authPkh, bipPath, SIGNER_LEDGER); err != nil { 257 | log.WithError(err).Error("Cannot save key/wallet to db") 258 | return err 259 | } 260 | 261 | s.Info.Pkh = authPkh 262 | s.Info.BipPath = bipPath 263 | 264 | log.WithFields(log.Fields{ 265 | "BakerPKH": authPkh, "BipPath": bipPath, 266 | }).Info("Saved authorized baking on ledger") 267 | 268 | // No errors; User confirmed key on device. All is good. 269 | return nil 270 | } 271 | 272 | // SaveSigner Saves Pkh and BipPath to DB 273 | func (s *LedgerSigner) SaveSigner() error { 274 | 275 | if err := s.storage.SaveLedgerToDB(s.Info.Pkh, s.Info.BipPath, SIGNER_LEDGER); err != nil { 276 | log.WithError(err).Error("Cannot save key/wallet to db") 277 | return err 278 | } 279 | 280 | return nil 281 | } 282 | -------------------------------------------------------------------------------- /bakinbacon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | "syscall" 11 | 12 | log "github.com/sirupsen/logrus" 13 | 14 | "bakinbacon/baconclient" 15 | "bakinbacon/notifications" 16 | "bakinbacon/payouts" 17 | "bakinbacon/storage" 18 | "bakinbacon/util" 19 | "bakinbacon/webserver" 20 | ) 21 | 22 | var ( 23 | bakinbacon *BakinBacon 24 | ) 25 | 26 | type BakinBacon struct { 27 | *baconclient.BaconClient 28 | *notifications.NotificationHandler 29 | *payouts.PayoutsHandler 30 | *storage.Storage 31 | *util.NetworkConstants 32 | Flags 33 | } 34 | 35 | //nolint:structcheck 36 | type Flags struct { 37 | network string 38 | logDebug bool 39 | logTrace bool 40 | dryRunEndorsement bool 41 | dryRunBake bool 42 | noPayouts bool 43 | webUiAddr string 44 | webUiPort int 45 | dataDir string 46 | } 47 | 48 | // TODO: Translations (https://www.transifex.com/bakinbacon/bakinbacon-core/content/) 49 | 50 | func main() { 51 | 52 | // Used throughout main 53 | var ( 54 | err error 55 | wg sync.WaitGroup 56 | ctx context.Context 57 | ) 58 | 59 | // Init the main server 60 | bakinbacon = &BakinBacon{} 61 | bakinbacon.parseArgs() 62 | 63 | // Logging 64 | setupLogging(bakinbacon.logDebug, bakinbacon.logTrace) 65 | 66 | // Clean exits 67 | shutdownChannel := setupCloseChannel() 68 | 69 | // Open/Init database 70 | bakinbacon.Storage, err = storage.InitStorage(bakinbacon.dataDir, bakinbacon.network) 71 | if err != nil { 72 | log.WithError(err).Fatal("Could not open storage") 73 | } 74 | 75 | // Start 76 | startMsg := fmt.Sprintf("=== BakinBacon %s (%s) ===", version, commitHash) 77 | log.Infof(startMsg) 78 | log.Infof("=== Network: %s ===", bakinbacon.network) 79 | 80 | // Global Notifications handler 81 | bakinbacon.NotificationHandler, err = notifications.NewHandler(bakinbacon.Storage) 82 | if err != nil { 83 | log.WithError(err).Error("Unable to load notifiers") 84 | } 85 | bakinbacon.SendNotification(startMsg, notifications.STARTUP) 86 | 87 | // Network constants 88 | bakinbacon.NetworkConstants, err = util.GetNetworkConstants(bakinbacon.network) 89 | if err != nil { 90 | log.WithError(err).Fatal("Cannot load network constants") 91 | } 92 | 93 | log.WithFields(log.Fields{ //nolint:wsl 94 | "BlocksPerCycle": bakinbacon.NetworkConstants.BlocksPerCycle, 95 | "BlocksPerCommitment": bakinbacon.NetworkConstants.BlocksPerCommitment, 96 | "TimeBetweenBlocks": bakinbacon.NetworkConstants.TimeBetweenBlocks, 97 | }).Debug("Loaded Network Constants") 98 | 99 | // Set up RPC polling-monitoring 100 | bakinbacon.BaconClient, err = baconclient.New( 101 | bakinbacon.NotificationHandler, bakinbacon.Storage, bakinbacon.NetworkConstants, shutdownChannel, &wg) 102 | if err != nil { 103 | log.WithError(err).Fatalf("Cannot create BaconClient") 104 | } 105 | 106 | // For managing rewards payouts 107 | bakinbacon.PayoutsHandler, err = payouts.NewPayoutsHandler( 108 | bakinbacon.BaconClient, bakinbacon.Storage, bakinbacon.NetworkConstants, bakinbacon.NotificationHandler, bakinbacon.noPayouts) 109 | if err != nil { 110 | log.WithError(err).Fatalf("Cannot create payouts handler") 111 | } 112 | 113 | // Version checking 114 | go bakinbacon.RunVersionCheck() 115 | 116 | // Start web UI 117 | // Template variables for the UI 118 | templateVars := webserver.TemplateVars{ 119 | Network: bakinbacon.network, 120 | BlocksPerCycle: bakinbacon.NetworkConstants.BlocksPerCycle, 121 | MinBlockTime: bakinbacon.NetworkConstants.TimeBetweenBlocks, 122 | UiBaseUrl: os.Getenv("UI_DEBUG"), 123 | } 124 | 125 | // Args for web server 126 | webServerArgs := webserver.WebServerArgs{ 127 | Client: bakinbacon.BaconClient, 128 | NotificationHandler: bakinbacon.NotificationHandler, 129 | PayoutsHandler: bakinbacon.PayoutsHandler, 130 | Storage: bakinbacon.Storage, 131 | BindAddr: bakinbacon.webUiAddr, 132 | BindPort: bakinbacon.webUiPort, 133 | TemplateVars: templateVars, 134 | ShutdownChannel: shutdownChannel, 135 | WG: &wg, 136 | } 137 | if err := webserver.Start(webServerArgs); err != nil { 138 | log.WithError(err).Error("Unable to start webserver UI") 139 | os.Exit(1) 140 | } 141 | 142 | // For canceling when new blocks appear 143 | _, ctxCancel := context.WithCancel(context.Background()) 144 | 145 | // Run checks against our address; silent mode = false 146 | _ = bakinbacon.CanBake(false) 147 | 148 | // Update bacon-status with most recent bake/endorse info 149 | bakinbacon.updateRecentBaconStatus() 150 | 151 | // loop forever, waiting for new blocks coming from the RPC monitors 152 | Main: 153 | for { 154 | 155 | select { 156 | case block := <-bakinbacon.NewBlockNotifier: 157 | 158 | // New block means to cancel any existing baking work as someone else beat us to it. 159 | // Noop on very first block from channel 160 | ctxCancel() 161 | 162 | // Create a new context for this run 163 | ctx, ctxCancel = context.WithCancel(context.Background()) 164 | 165 | // If we can't bake, no need to do try and do anything else 166 | // This check is silent = true on success 167 | if !bakinbacon.CanBake(true) { 168 | continue 169 | } 170 | 171 | wg.Add(1) 172 | go bakinbacon.handleEndorsement(ctx, &wg, *block) 173 | 174 | wg.Add(1) 175 | go bakinbacon.revealNonces(ctx, &wg, *block) 176 | 177 | wg.Add(1) 178 | go bakinbacon.handleBake(ctx, &wg, *block) 179 | 180 | wg.Add(1) 181 | go bakinbacon.PayoutsHandler.HandlePayouts(ctx, &wg, *block) 182 | 183 | // 184 | // Utility 185 | // 186 | 187 | // Update UI with next rights 188 | go bakinbacon.updateCycleRightsStatus(block.Metadata.Level) 189 | 190 | // Pre-fetch rights to DB as both backup and for UI display 191 | go bakinbacon.prefetchCycleRights(block.Metadata.Level) 192 | 193 | case <-shutdownChannel: 194 | log.Warn("Shutting things down...") 195 | ctxCancel() 196 | bakinbacon.BaconClient.Shutdown() 197 | break Main 198 | } 199 | } 200 | 201 | // Wait for threads to finish 202 | wg.Wait() 203 | 204 | // Clean close DB, logs 205 | bakinbacon.Storage.CloseDb() 206 | closeLogging() 207 | 208 | os.Exit(0) 209 | } 210 | 211 | func setupCloseChannel() chan interface{} { 212 | 213 | // Create channels for signals 214 | signalChan := make(chan os.Signal, 1) 215 | closingChan := make(chan interface{}, 1) 216 | 217 | signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) 218 | 219 | go func() { 220 | <-signalChan 221 | close(closingChan) 222 | }() 223 | 224 | return closingChan 225 | } 226 | 227 | func (bb *BakinBacon) parseArgs() { 228 | 229 | // Args 230 | flag.StringVar(&bb.network, "network", util.NETWORK_HANGZHOUNET, fmt.Sprintf("Which network to use: %s", util.AvailableNetworks())) 231 | 232 | flag.BoolVar(&bb.logDebug, "debug", false, "Enable debug-level logging") 233 | flag.BoolVar(&bb.logTrace, "trace", false, "Enable trace-level logging") 234 | 235 | flag.BoolVar(&bb.dryRunEndorsement, "dry-run-endorse", false, "Compute, but don't inject endorsements") 236 | flag.BoolVar(&bb.dryRunBake, "dry-run-bake", false, "Compute, but don't inject blocks") 237 | 238 | flag.BoolVar(&bb.noPayouts, "no-payouts", false, "Disable payouts within BakinBacon") 239 | 240 | flag.StringVar(&bb.webUiAddr, "webuiaddr", "127.0.0.1", "Address on which to bind web UI server") 241 | flag.IntVar(&bb.webUiPort, "webuiport", 8082, "Port on which to bind web UI server") 242 | 243 | flag.StringVar(&bb.dataDir, "datadir", "./", "Location of database") 244 | 245 | printVersion := flag.Bool("version", false, "Show version and exit") 246 | 247 | flag.Parse() 248 | 249 | // Sanity 250 | if !util.IsValidNetwork(bb.network) { 251 | log.Errorf("Unknown network: %s", bb.network) 252 | flag.Usage() 253 | os.Exit(1) 254 | } 255 | 256 | // Handle print version and exit 257 | if *printVersion { 258 | log.Printf("Bakin'Bacon %s (%s)", version, commitHash) 259 | log.Printf("https://github.com/bakingbacon/bakinbacon") 260 | os.Exit(0) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /baconsigner/baconsigner.go: -------------------------------------------------------------------------------- 1 | package baconsigner 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | 7 | "github.com/Messer4/base58check" 8 | "github.com/pkg/errors" 9 | 10 | log "github.com/sirupsen/logrus" 11 | 12 | "bakinbacon/storage" 13 | ) 14 | 15 | const ( 16 | SIGNER_WALLET = 1 17 | SIGNER_LEDGER = 2 18 | ) 19 | 20 | var ( 21 | NO_SIGNER_TYPE = errors.New("No signer type defined") 22 | ) 23 | 24 | type BaconSigner struct { 25 | BakerPkh string 26 | signerType int 27 | storage *storage.Storage 28 | } 29 | 30 | // SignOperationOutput contains an operation with the signature appended, and the signature 31 | type SignOperationOutput struct { 32 | SignedOperation string 33 | Signature string 34 | EDSig string 35 | } 36 | 37 | // New 38 | func New(db *storage.Storage) (*BaconSigner, error) { 39 | 40 | bs := &BaconSigner{ 41 | storage: db, 42 | } 43 | 44 | // Get which signing method (wallet or ledger), so we can perform sanity checks 45 | signerType, err := bs.storage.GetSignerType() 46 | if err != nil { 47 | return bs, errors.Wrap(err, "Unable to get signer type from DB") 48 | } 49 | bs.signerType = signerType 50 | 51 | switch bs.signerType { 52 | case SIGNER_WALLET: 53 | if err := InitWalletSigner(db); err != nil { 54 | return bs, errors.Wrap(err, "Cannot init wallet signer") 55 | } 56 | case SIGNER_LEDGER: 57 | if err := InitLedgerSigner(db); err != nil { 58 | return bs, errors.Wrap(err, "Cannot init ledger signer") 59 | } 60 | default: 61 | log.WithField("Type", signerType).Warn("No signer type defined. New setup?") 62 | } 63 | 64 | return bs, nil 65 | } 66 | 67 | // SignerStatus returns error if baking is not configured. Delegate secret key must be configured in DB, 68 | // and signer type must also be set and wallet must be loadable 69 | func (s *BaconSigner) SignerStatus(silent bool) error { 70 | 71 | // Try to load the bakers SK 72 | if err := s.LoadDelegate(silent); err != nil { 73 | return errors.Wrap(err, "Loading Delegate") 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func (s *BaconSigner) LoadDelegate(silent bool) error { 80 | 81 | var err error 82 | 83 | _, s.BakerPkh, err = s.storage.GetDelegate() 84 | if err != nil { 85 | log.WithError(err).Error("Unable to load delegate from DB") 86 | return err 87 | } 88 | 89 | if s.BakerPkh == "" { 90 | // Missing delegate; cannot bake/endorse/nonce; User needs to configure via UI 91 | return errors.New("No delegate key defined") 92 | } 93 | 94 | if !silent { 95 | log.WithField("Delegate", s.BakerPkh).Info("Loaded delegate public key hash from DB") 96 | } 97 | 98 | return nil 99 | } 100 | 101 | // ConfirmBakingPkh Confirms action on ledger; Not applicable to signer 102 | func (s *BaconSigner) ConfirmBakingPkh(pkh, bip string) error { 103 | 104 | if err := L.ConfirmBakingPkh(pkh, bip); err != nil { 105 | return errors.Wrap(err, "Cannot confirm baking address") 106 | } 107 | 108 | // Set BaconSigner if all is good 109 | s.signerType = SIGNER_LEDGER 110 | 111 | return nil 112 | } 113 | 114 | // GetPublicKey Gets the public key, and public key hash, depending on signer type 115 | func (s *BaconSigner) GetPublicKey() (string, string, error) { 116 | 117 | switch s.signerType { 118 | case SIGNER_WALLET: 119 | return W.GetPublicKey() 120 | case SIGNER_LEDGER: 121 | return L.GetPublicKey() 122 | } 123 | 124 | return "", "", NO_SIGNER_TYPE 125 | } 126 | 127 | // GenerateNewKey Generates new key; Not applicable to Ledger 128 | func (s *BaconSigner) GenerateNewKey() (string, string, error) { 129 | 130 | sk, pkh, err := GenerateNewKey(s.storage) 131 | if err != nil { 132 | return "", "", errors.Wrap(err, "Cannot generate new key") 133 | } 134 | 135 | // Set if all is good 136 | s.signerType = SIGNER_WALLET 137 | 138 | return sk, pkh, nil 139 | } 140 | 141 | // ImportSecretKey Imports a secret key; Not applicable to ledger 142 | func (s *BaconSigner) ImportSecretKey(k string) (string, string, error) { 143 | 144 | sk, pkh, err := ImportSecretKey(k, s.storage) 145 | if err != nil { 146 | return "", "", errors.Wrap(err, "Cannot import secret key") 147 | } 148 | 149 | // Set if all is good 150 | s.signerType = SIGNER_WALLET 151 | 152 | return sk, pkh, nil 153 | } 154 | 155 | // TestLedger Will check if Ledger is plugged in and app is open; Not applicable to wallet 156 | func (s *BaconSigner) TestLedger() (*LedgerInfo, error) { 157 | return TestLedger(s.storage) 158 | } 159 | 160 | // SaveSigner Saves signer config to DB 161 | func (s *BaconSigner) SaveSigner() error { 162 | 163 | switch s.signerType { 164 | case SIGNER_WALLET: 165 | return W.SaveSigner() 166 | case SIGNER_LEDGER: 167 | return L.SaveSigner() 168 | } 169 | 170 | return NO_SIGNER_TYPE 171 | } 172 | 173 | // Close ledger or wallet 174 | func (s *BaconSigner) Close() { 175 | 176 | if s.signerType == SIGNER_LEDGER { 177 | L.Close() 178 | } 179 | } 180 | 181 | // Signing Functions 182 | 183 | func (s *BaconSigner) SignEndorsement(endorsementBytes, chainID string) (SignOperationOutput, error) { 184 | return s.signGeneric(endorsementprefix, endorsementBytes, chainID) 185 | } 186 | 187 | func (s *BaconSigner) SignBlock(blockBytes, chainID string) (SignOperationOutput, error) { 188 | return s.signGeneric(blockprefix, blockBytes, chainID) 189 | } 190 | 191 | func (s *BaconSigner) SignNonce(nonceBytes string, chainID string) (SignOperationOutput, error) { 192 | // Nonce reveals have the same watermark as endorsements 193 | return s.signGeneric(endorsementprefix, nonceBytes, chainID) 194 | } 195 | 196 | func (s *BaconSigner) SignReveal(revealBytes string) (SignOperationOutput, error) { 197 | return s.signGeneric(genericopprefix, revealBytes, "") 198 | } 199 | 200 | func (s *BaconSigner) SignTransaction(trxBytes string) (SignOperationOutput, error) { 201 | return s.signGeneric(genericopprefix, trxBytes, "") 202 | } 203 | 204 | func (s *BaconSigner) SignSetDelegate(delegateBytes string) (SignOperationOutput, error) { 205 | return s.signGeneric(genericopprefix, delegateBytes, "") 206 | } 207 | 208 | func (s *BaconSigner) SignProposalVote(proposalBytes string) (SignOperationOutput, error) { 209 | return s.signGeneric(genericopprefix, proposalBytes, "") 210 | } 211 | 212 | // Generic raw signing function 213 | // Takes the incoming operation hex-bytes and signs using whichever wallet type is in use 214 | func (s *BaconSigner) signGeneric(opPrefix prefix, incOpHex, chainID string) (SignOperationOutput, error) { 215 | 216 | // Base bytes of operation; all ops begin with prefix 217 | opBytes := opPrefix 218 | 219 | if chainID != "" { 220 | // Strip off the network watermark (prefix), and then base58 decode the chain id string (ie: NetXUdfLh6Gm88t) 221 | chainIdBytes := b58cdecode(chainID, networkprefix) 222 | // fmt.Println("ChainID: ", chainID) 223 | // fmt.Println("ChainIDByt: ", chainIdBytes) 224 | // fmt.Println("ChainIDHex: ", hex.EncodeToString(chainIdBytes)) 225 | 226 | opBytes = append(opBytes, chainIdBytes...) 227 | } 228 | 229 | // Decode the incoming operational hex to bytes 230 | incOpBytes, err := hex.DecodeString(incOpHex) 231 | if err != nil { 232 | return SignOperationOutput{}, errors.Wrap(err, "Failed to sign operation") 233 | } 234 | // fmt.Println("IncOpHex: ", incOpHex) 235 | // fmt.Println("IncOpBytes: ", incOpBytes) 236 | 237 | // Append incoming op bytes to either prefix, or prefix + chainId 238 | opBytes = append(opBytes, incOpBytes...) 239 | 240 | // Convert op bytes back to hex; anyone need this? 241 | // finalOpHex := hex.EncodeToString(opBytes) 242 | // fmt.Println("ToSignBytes: ", opBytes) 243 | // fmt.Println("ToSignByHex: ", finalOpHex) 244 | 245 | edSig, err := func(b []byte) (string, error) { 246 | switch s.signerType { 247 | case SIGNER_WALLET: 248 | return W.SignBytes(b) 249 | case SIGNER_LEDGER: 250 | return L.SignBytes(b) 251 | } 252 | return "", NO_SIGNER_TYPE 253 | }(opBytes) 254 | 255 | if err != nil { 256 | return SignOperationOutput{}, errors.Wrap(err, "Failed sign bytes") 257 | } 258 | 259 | // Decode out the signature from the operation 260 | decodedSig, err := decodeSignature(edSig) 261 | if err != nil { 262 | return SignOperationOutput{}, errors.Wrap(err, "Failed to decode signed block") 263 | } 264 | 265 | return SignOperationOutput{ 266 | SignedOperation: fmt.Sprintf("%s%s", incOpHex, decodedSig), 267 | Signature: decodedSig, 268 | EDSig: edSig, 269 | }, nil 270 | } 271 | 272 | // Helper function to return the decoded signature 273 | func decodeSignature(signature string) (string, error) { 274 | 275 | decBytes, err := base58check.Decode(signature) 276 | if err != nil { 277 | return "", errors.Wrap(err, "failed to decode signature") 278 | } 279 | 280 | decodedSigHex := hex.EncodeToString(decBytes) 281 | 282 | // sanity 283 | if len(decodedSigHex) > 10 { 284 | decodedSigHex = decodedSigHex[10:] 285 | } else { 286 | return "", errors.Wrap(err, "decoded signature is invalid length") 287 | } 288 | 289 | return decodedSigHex, nil 290 | } 291 | -------------------------------------------------------------------------------- /webserver/src/voting.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useEffect } from 'react'; 2 | 3 | import Alert from 'react-bootstrap/Alert' 4 | import Button from 'react-bootstrap/Button'; 5 | import Col from 'react-bootstrap/Col'; 6 | import Card from 'react-bootstrap/Card'; 7 | import ListGroup from 'react-bootstrap/ListGroup'; 8 | import Loader from "react-loader-spinner"; 9 | import Row from 'react-bootstrap/Row'; 10 | 11 | import ToasterContext from './toaster.js'; 12 | import { BaconAlert, apiRequest } from './util.js'; 13 | 14 | import "react-loader-spinner/dist/loader/css/react-spinner-loader.css"; 15 | 16 | const Voting = (props) => { 17 | 18 | const { delegate } = props; 19 | 20 | const [ votingPhase, setVotingPhase ] = useState("proposal"); 21 | const [ votingPeriod, setVotingPeriod ] = useState(0); 22 | const [ remainingPhaseBlocks, setRemainingBlocks ] = useState(0); 23 | const [ explorationProposals, setExplorationProposals ] = useState([]); 24 | const [ currentProposal, setCurrentProposal ] = useState(""); 25 | const [ hasVoted, setHasVoted ] = useState(false); 26 | const [ isCastingVote, setIsCastingVote ] = useState(false); 27 | const [ alert, setAlert ] = useState({}); 28 | const [ isLoading, setIsLoading ] = useState(true); 29 | const addToast = useContext(ToasterContext); 30 | 31 | const baseVotesApiUrl = "http://"+window.NETWORK+"-us.rpc.bakinbacon.io/chains/main/blocks/head/votes" 32 | 33 | useEffect(() => { 34 | 35 | // Determine current period 36 | const currentPeriodApi = baseVotesApiUrl + "/current_period" 37 | apiRequest(currentPeriodApi) 38 | .then((data) => { 39 | 40 | setVotingPeriod(data.voting_period.index); 41 | setVotingPhase(data.voting_period.kind || "Unknown"); 42 | setRemainingBlocks(data.remaining); 43 | 44 | // proposal -> exploration -> cooldown -> promotion -> adoption 45 | 46 | if (votingPhase === "proposal") { 47 | // Fetch current list of proposals and display 48 | const activeProposalsApi = baseVotesApiUrl + "/proposals" 49 | apiRequest(activeProposalsApi) 50 | .then((data) => { 51 | setExplorationProposals(data); 52 | }) 53 | .catch((errMsg) => { throw errMsg; }) 54 | } 55 | 56 | if (votingPhase === "exploration" || votingPhase === "cooldown" || votingPhase === "promotion") { 57 | 58 | // Fetch current proposal 59 | const currentProposalApi = baseVotesApiUrl + "/current_proposal" 60 | apiRequest(currentProposalApi) 61 | .then((data) => { 62 | setCurrentProposal(data) 63 | }) 64 | .catch((errMsg) => { throw errMsg; }) 65 | 66 | // If exploration, get list of votes to check if already voted 67 | if (votingPhase === "exploration") { 68 | const ballotListApi = baseVotesApiUrl + "/ballot_list" 69 | apiRequest(ballotListApi) 70 | .then((data) => { 71 | const ballots = data; 72 | ballots.forEach((e) => { 73 | if (e.pkh === delegate) { 74 | setHasVoted(true); 75 | } 76 | }) 77 | }) 78 | .catch((errMsg) => { throw errMsg; }) 79 | } 80 | } 81 | }) 82 | .catch((errMsg) => { 83 | console.log(errMsg); 84 | addToast({ 85 | title: "Loading Voting Period Error", 86 | msg: errMsg, 87 | type: "danger", 88 | }); 89 | }) 90 | .finally(() => { 91 | setIsLoading(false); 92 | }); 93 | 94 | // eslint-disable-next-line react-hooks/exhaustive-deps 95 | }, []); 96 | 97 | const castUpVote = (proposal) => { 98 | 99 | setIsCastingVote(true); 100 | 101 | // TODO: Check if already voted? 102 | 103 | const upVoteApiUrl = window.BASE_URL + "/api/voting/upvote" 104 | const requestOptions = { 105 | method: 'POST', 106 | headers: { 'Content-Type': 'application/json' }, 107 | body: JSON.stringify({ 108 | "p": proposal, 109 | "i": votingPeriod, 110 | }) 111 | }; 112 | 113 | apiRequest(upVoteApiUrl, requestOptions) 114 | .then((data) => { 115 | setAlert({ 116 | type: "success", 117 | msg: "Successfully cast vote for proposal: "+proposal+"!" 118 | }); 119 | }) 120 | .catch((errMsg) => { 121 | console.log(errMsg); 122 | addToast({ 123 | title: "Loading Voting Period Error", 124 | msg: errMsg, 125 | type: "danger", 126 | }); 127 | }) 128 | .finally(() => { 129 | setIsCastingVote(false); 130 | }); 131 | } 132 | 133 | const castYayNayPassVote = (proposal, vote) => { 134 | console.log(proposal); 135 | console.log(vote); 136 | } 137 | 138 | 139 | if (isLoading) { 140 | return ( 141 | 142 | 143 |
Loading Voting Info... 144 | 145 |
146 | ) 147 | } 148 | 149 | if (votingPhase === "proposal") { 150 | return ( 151 | <> 152 | 153 | 154 | 155 | Voting Governance 156 | 157 | Proposal Period 158 | Tezos is currently in the proposal period where bakers can submit new protocols for voting, and cast votes on existing proposals. If any have been submitted so far, you can see them below, and can cast your vote using the button. 159 | This period will last for {remainingPhaseBlocks} more blocks. After this time, the proposal with the most votes will move into the exploration period where you will be able to cast another vote. If no proposals have been submitted by then, the proposal phase starts over. 160 | 161 | { isCastingVote ? 162 | <> 163 | 164 | 165 | 166 |
167 | Casting your vote. If using a ledger device, please look at the device and confirm the vote. 168 | 169 |
170 | 171 | : 172 | <> 173 | Current Proposals 174 | 175 | { explorationProposals.length === 0 ? No proposals have been submitted. : 176 | explorationProposals.map((o, i) => { 177 | return 178 | 179 | {o[0]} 180 | 181 | })} 182 | 183 | 184 | } 185 |
186 |
187 | 188 |
189 | 190 | 191 | ) 192 | } 193 | 194 | if (votingPhase === "exploration") { 195 | return ( 196 | <> 197 | 198 | 199 | 200 | Voting Governance 201 | 202 | Exploration Period 203 | Tezos is currently in the exploration period where bakers can vote a new protocol. 204 | This period will last for {remainingPhaseBlocks} more blocks. 205 | Below, you will see the currently proposed protocol. You can click on the proposal to view the publicly posted information. Cast your vote by clicking on the appropriate button next to the proposal. 206 | Current Proposal 207 | 208 | 209 | 210 | 211 | {currentProposal} 212 | 213 | { hasVoted && You have already voted! } 214 | 215 | 216 | 217 | 218 | 219 | 220 | ) 221 | } 222 | 223 | if (votingPhase === "cooldown") { 224 | return ( 225 | 226 | 227 | 228 | Voting Governance 229 | 230 | There is currently no active voting period. Tezos is currently in the cooldown period where the initially chosen protocol, {currentProposal}, is being tested. 231 | This period will last for {remainingPhaseBlocks} more blocks. After this, voting will open once again for the promotion period. 232 | 233 | 234 | 235 | 236 | ) 237 | } 238 | 239 | if (votingPhase === "adoption") { 240 | return ( 241 | 242 | 243 | 244 | Voting Governance 245 | 246 | There is currently no active voting period. Tezos is currently in the proposal period where bakers can submit new protocols for voting. 247 | This period will last for {remainingPhaseBlocks} more blocks. If no proposals have been submitted by then, the proposal phase starts over. 248 | 249 | 250 | 251 | 252 | ) 253 | } 254 | 255 | 256 | return ( 257 | Uh oh... something went wrong. You should refresh your browser and start over. 258 | ) 259 | }; 260 | 261 | export default Voting; 262 | -------------------------------------------------------------------------------- /prefetch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | 6 | "github.com/pkg/errors" 7 | 8 | "github.com/bakingbacon/go-tezos/v4/rpc" 9 | ) 10 | 11 | // Update BaconStatus with the most recent information from DB. This 12 | // is done to initialize BaconStatus with values, otherwise status does 13 | // not update until next bake/endorse. 14 | func (bb *BakinBacon) updateRecentBaconStatus() { 15 | 16 | // Update baconClient.Status with most recent endorsement 17 | recentEndorsementLevel, recentEndorsementHash, err := bb.Storage.GetRecentEndorsement() 18 | if err != nil { 19 | log.WithError(err).Error("Unable to get recent endorsement") 20 | } 21 | 22 | bb.Status.SetRecentEndorsement(recentEndorsementLevel, bb.getCycleFromLevel(recentEndorsementLevel), recentEndorsementHash) 23 | 24 | // Update baconClient.Status with most recent bake 25 | recentBakeLevel, recentBakeHash, err := bb.Storage.GetRecentBake() 26 | if err != nil { 27 | log.WithError(err).Error("Unable to get recent bake") 28 | } 29 | 30 | bb.Status.SetRecentBake(recentBakeLevel, bb.getCycleFromLevel(recentBakeLevel), recentBakeHash) 31 | } 32 | 33 | // Called on each new block; update BaconStatus with next opportunity for bakes/endorses 34 | func (bb *BakinBacon) updateCycleRightsStatus(metadataLevel rpc.Level) { 35 | 36 | nextCycle := metadataLevel.Cycle + 1 37 | 38 | // Update our baconStatus with next endorsement level and next baking right. 39 | // If this returns err, it means there was no bucket data which means 40 | // we have never fetched current cycle rights and should do so asap 41 | nextEndorsingLevel, highestFetchedCycle, err := bb.Storage.GetNextEndorsingRight(metadataLevel.Level) 42 | if err != nil { 43 | log.WithError(err).Error("GetNextEndorsingRight") 44 | } 45 | 46 | // Update BaconClient status, even if next level is 0 (none found) 47 | nextEndorsingCycle := bb.getCycleFromLevel(nextEndorsingLevel) 48 | bb.Status.SetNextEndorsement(nextEndorsingLevel, nextEndorsingCycle) 49 | 50 | log.WithFields(log.Fields{ 51 | "Level": nextEndorsingLevel, "Cycle": nextEndorsingCycle, 52 | }).Trace("Next Endorsing") 53 | 54 | // If next level is 0, check to see if we need to fetch cycle 55 | if nextEndorsingLevel == 0 { 56 | switch { 57 | case highestFetchedCycle < metadataLevel.Cycle: 58 | log.WithField("Cycle", metadataLevel.Cycle).Info("Fetch Cycle Endorsing Rights") 59 | 60 | go bb.fetchEndorsingRights(metadataLevel, metadataLevel.Cycle) 61 | 62 | case highestFetchedCycle < nextCycle: 63 | log.WithField("Cycle", nextCycle).Info("Fetch Next Cycle Endorsing Rights") 64 | 65 | go bb.fetchEndorsingRights(metadataLevel, nextCycle) 66 | } 67 | } 68 | 69 | // 70 | // Next baking right; similar logic to above 71 | // 72 | nextBakeLevel, nextBakePriority, highestFetchedCycle, err := bb.Storage.GetNextBakingRight(metadataLevel.Level) 73 | if err != nil { 74 | log.WithError(err).Error("GetNextEndorsingRight") 75 | } 76 | 77 | // Update BaconClient status, even if next level is 0 (none found) 78 | nextBakeCycle := bb.getCycleFromLevel(nextBakeLevel) 79 | bb.Status.SetNextBake(nextBakeLevel, nextBakeCycle, nextBakePriority) 80 | 81 | log.WithFields(log.Fields{ 82 | "Level": nextBakeLevel, "Cycle": nextBakeCycle, "Priority": nextBakePriority, 83 | }).Trace("Next Baking") 84 | 85 | if nextBakeLevel == 0 { 86 | switch { 87 | case highestFetchedCycle < metadataLevel.Cycle: 88 | log.WithField("Cycle", metadataLevel.Cycle).Info("Fetch Cycle Baking Rights") 89 | 90 | go bb.fetchBakingRights(metadataLevel, metadataLevel.Cycle) 91 | 92 | case highestFetchedCycle < nextCycle: 93 | log.WithField("Cycle", nextCycle).Info("Fetch Next Cycle Baking Rights") 94 | 95 | go bb.fetchBakingRights(metadataLevel, nextCycle) 96 | } 97 | } 98 | } 99 | 100 | // Called on each new block; Only processes every 1024 blocks 101 | // Fetches the bake/endorse rights for the next cycle and stores to DB 102 | func (bb *BakinBacon) prefetchCycleRights(metadataLevel rpc.Level) { 103 | 104 | // We only prefetch every 1024 levels 105 | if metadataLevel.Level % 1024 != 0 { 106 | return 107 | } 108 | 109 | nextCycle := metadataLevel.Cycle + 1 110 | 111 | log.WithField("NextCycle", nextCycle).Info("Pre-fetching rights for next cycle") 112 | 113 | go bb.fetchEndorsingRights(metadataLevel, nextCycle) 114 | go bb.fetchBakingRights(metadataLevel, nextCycle) 115 | } 116 | 117 | func (bb *BakinBacon) fetchEndorsingRights(metadataLevel rpc.Level, cycleToFetch int) { 118 | 119 | if bb.Signer.BakerPkh == "" { 120 | log.Error("Cannot fetch endorsing rights; No baker configured") 121 | return 122 | } 123 | 124 | // Due to inefficiencies in tezos-node RPC introduced by Granada, 125 | // we cannot query all rights of a delegate based on cycle. 126 | // This produces too much load on the node and usually times out. 127 | // 128 | // Instead, we make an insane number of fast RPCs to get rights 129 | // per level for the reminder of this cycle, or for the next cycle. 130 | 131 | blocksPerCycle := bb.NetworkConstants.BlocksPerCycle 132 | 133 | levelToStart, levelToEnd, err := levelToStartEnd(metadataLevel, blocksPerCycle, cycleToFetch) 134 | if err != nil { 135 | log.WithError(err).Error("Unable to fetch endorsing rights") 136 | return 137 | } 138 | 139 | // Can't have more rights than blocks per cycle; set the 140 | // capacity of the slice to avoid reallocation on append 141 | allEndorsingRights := make([]rpc.EndorsingRights, 0, blocksPerCycle) 142 | 143 | // Range from start to end, fetch rights per level 144 | for level := levelToStart; level < levelToEnd; level++ { 145 | 146 | // Chill on logging 147 | if level % 256 == 0 { 148 | log.WithFields(log.Fields{ 149 | "S": levelToStart, "L": level, "E": levelToEnd, 150 | }).Trace("Fetched endorsing rights") 151 | } 152 | 153 | endorsingRightsFilter := rpc.EndorsingRightsInput{ 154 | BlockID: &rpc.BlockIDHead{}, 155 | Level: level, 156 | Delegate: bb.Signer.BakerPkh, 157 | } 158 | 159 | resp, endorsingRights, err := bb.Current.EndorsingRights(endorsingRightsFilter) 160 | if err != nil { 161 | log.WithError(err).WithFields(log.Fields{ 162 | "Request": resp.Request.URL, "Response": string(resp.Body()), 163 | }).Error("Unable to fetch endorsing rights") 164 | 165 | return 166 | } 167 | 168 | // Append this levels' rights, if exists 169 | if len(endorsingRights) > 0 { 170 | allEndorsingRights = append(allEndorsingRights, endorsingRights[0]) 171 | } 172 | } 173 | 174 | log.WithFields(log.Fields{ 175 | "Cycle": cycleToFetch, "LS": levelToStart, "LE": levelToEnd, "Num": len(allEndorsingRights), 176 | }).Debug("Prefetched Endorsing Rights") 177 | 178 | // Save rights to DB, even if len == 0 so that it is noted we queried this cycle 179 | if err := bb.Storage.SaveEndorsingRightsForCycle(cycleToFetch, allEndorsingRights); err != nil { 180 | log.WithError(err).Error("Unable to save endorsing rights for cycle") 181 | } 182 | } 183 | 184 | func (bb *BakinBacon) fetchBakingRights(metadataLevel rpc.Level, cycleToFetch int) { 185 | 186 | if bb.Signer.BakerPkh == "" { 187 | log.Error("Cannot fetch baking rights; No baker configured") 188 | return 189 | } 190 | 191 | blocksPerCycle := bb.NetworkConstants.BlocksPerCycle 192 | 193 | levelToStart, levelToEnd, err := levelToStartEnd(metadataLevel, blocksPerCycle, cycleToFetch) 194 | if err != nil { 195 | log.WithError(err).Error("Unable to fetch baking rights") 196 | return 197 | } 198 | 199 | allBakingRights := make([]rpc.BakingRights, 0, blocksPerCycle) 200 | 201 | // Range from start to end, fetch rights per level 202 | for level := levelToStart; level < levelToEnd; level++ { 203 | 204 | bakingRightsFilter := rpc.BakingRightsInput{ 205 | BlockID: &rpc.BlockIDHead{}, 206 | Level: level, 207 | Delegate: bb.Signer.BakerPkh, 208 | } 209 | 210 | resp, bakingRights, err := bb.Current.BakingRights(bakingRightsFilter) 211 | if err != nil { 212 | log.WithError(err).WithFields(log.Fields{ 213 | "Request": resp.Request.URL, "Response": string(resp.Body()), 214 | }).Error("Unable to fetch next cycle baking rights") 215 | 216 | return 217 | } 218 | 219 | // If have rights and priority is < max, append to slice 220 | if len(bakingRights) > 0 && bakingRights[0].Priority < MAX_BAKE_PRIORITY { 221 | allBakingRights = append(allBakingRights, bakingRights[0]) 222 | } 223 | } 224 | 225 | // Got any rights? 226 | log.WithFields(log.Fields{ 227 | "Cycle": cycleToFetch, "LS": levelToStart, "LE": levelToEnd, "Num": len(allBakingRights), "MaxPriority": MAX_BAKE_PRIORITY, 228 | }).Info("Prefetched Baking Rights") 229 | 230 | // Save filtered rights to DB, even if len == 0 so that it is noted we queried this cycle 231 | if err := bb.Storage.SaveBakingRightsForCycle(cycleToFetch, allBakingRights); err != nil { 232 | log.WithError(err).Error("Unable to save baking rights for cycle") 233 | } 234 | } 235 | 236 | func levelToStartEnd(metadataLevel rpc.Level, blocksPerCycle, cycleToFetch int) (int, int, error) { 237 | 238 | var levelToStart, levelToEnd int 239 | levelsRemainingInCycle := blocksPerCycle - metadataLevel.CyclePosition 240 | 241 | // Are we fetching remaining rights in this level? 242 | if cycleToFetch == metadataLevel.Cycle { 243 | 244 | levelToStart = metadataLevel.Level 245 | levelToEnd = levelToStart + levelsRemainingInCycle + 1 246 | 247 | } else if cycleToFetch == (metadataLevel.Cycle + 1) { 248 | 249 | levelToStart = metadataLevel.Level + levelsRemainingInCycle 250 | levelToEnd = levelToStart + blocksPerCycle + 1 251 | 252 | } else { 253 | log.WithFields(log.Fields{ 254 | "CycleToFetch": cycleToFetch, "CurrentCycle": metadataLevel.Cycle, 255 | }).Error("Unable to fetch endorsing rights") 256 | return 0, 0, errors.New("Unable to calculate start/end") 257 | } 258 | 259 | return levelToStart, levelToEnd, nil 260 | } 261 | 262 | func (bb *BakinBacon) getCycleFromLevel(l int) int { 263 | 264 | gal := bb.NetworkConstants.GranadaActivationLevel 265 | gac := bb.NetworkConstants.GranadaActivationCycle 266 | 267 | // If level is before Granada activation, calculation is simple 268 | if l <= gal { 269 | return int(l / bb.NetworkConstants.BlocksPerCycle) 270 | } 271 | 272 | // If level is after Granada activation, must take in to account the 273 | // change in number of blocks per cycle 274 | return int(((l - gal) / bb.NetworkConstants.BlocksPerCycle) + gac) 275 | } 276 | --------------------------------------------------------------------------------