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
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 |
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 |
Ensure Ledger device is plugged in to USB port on computer running Bakin'Bacon.
110 |
Make sure Ledger is unlocked.
111 |
Install the 'Tezos Baking' application, and ensure it is open.
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------