-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: Implements census parse and mailing cli
* Adds cli logic * Adds census and voter collections to MongoDB storage
- Loading branch information
Showing
10 changed files
with
836 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/hex" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"math/big" | ||
"net/url" | ||
"os" | ||
"time" | ||
|
||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/google/uuid" | ||
"github.com/vocdoni/saas-backend/db" | ||
"go.vocdoni.io/dvote/api" | ||
"go.vocdoni.io/dvote/apiclient" | ||
"go.vocdoni.io/dvote/crypto/ethereum" | ||
"go.vocdoni.io/dvote/log" | ||
"go.vocdoni.io/dvote/types" | ||
"go.vocdoni.io/dvote/util" | ||
) | ||
|
||
type Config struct { | ||
Accounts []Account `json:"accounts"` | ||
LastAccountUsed int `json:"lastAccountUsed"` | ||
Host *url.URL `json:"host"` | ||
Token *uuid.UUID `json:"token"` | ||
} | ||
|
||
func (c *Config) Load(filepath string) error { | ||
data, err := os.ReadFile(filepath) | ||
if err != nil { | ||
if errors.Is(err, os.ErrNotExist) { | ||
c.LastAccountUsed = -1 | ||
return nil | ||
} | ||
return err | ||
} | ||
return json.Unmarshal(data, c) | ||
} | ||
|
||
func (c *Config) Save(filepath string) error { | ||
data, err := json.Marshal(c) | ||
if err != nil { | ||
return err | ||
} | ||
return os.WriteFile(filepath, data, 0o600) | ||
} | ||
|
||
type Account struct { | ||
PrivKey types.HexBytes `json:"privKey"` | ||
Memo string `json:"memo"` | ||
Address common.Address `json:"address"` | ||
PublicKey types.HexBytes `json:"pubKey"` | ||
} | ||
|
||
type CensusCLI struct { | ||
filepath string | ||
config *Config | ||
api *apiclient.HTTPclient | ||
chainID string | ||
csvFile string | ||
db *db.MongoStorage | ||
salt string | ||
|
||
currentAccount int | ||
} | ||
|
||
func NewCensusCLI(configFile, host, csvFile, salt string, db *db.MongoStorage) (*CensusCLI, error) { | ||
cfg := Config{} | ||
if err := cfg.Load(configFile); err != nil { | ||
return nil, err | ||
} | ||
if cfg.Token == nil { | ||
t := uuid.New() | ||
cfg.Token = &t | ||
} else { | ||
log.Infof("new bearer auth token %s", *cfg.Token) | ||
} | ||
|
||
var err error | ||
if host != "" { | ||
cfg.Host, err = url.Parse(host) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
if cfg.Host == nil { | ||
return nil, fmt.Errorf("no API server host configured") | ||
} | ||
|
||
api, err := apiclient.NewWithBearer(host, cfg.Token) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if len(cfg.Accounts)-1 >= cfg.LastAccountUsed && cfg.LastAccountUsed >= 0 { | ||
log.Infof("using account %d", cfg.LastAccountUsed) | ||
if err := api.SetAccount(cfg.Accounts[cfg.LastAccountUsed].PrivKey.String()); err != nil { | ||
return nil, err | ||
} | ||
} | ||
return &CensusCLI{ | ||
filepath: configFile, | ||
config: &cfg, | ||
api: api, | ||
chainID: api.ChainID(), | ||
csvFile: csvFile, | ||
db: db, | ||
currentAccount: cfg.LastAccountUsed, | ||
salt: salt, | ||
}, nil | ||
} | ||
|
||
func (v *CensusCLI) useAccount(index int) error { | ||
if index >= len(v.config.Accounts) { | ||
return fmt.Errorf("account %d does not exist", index) | ||
} | ||
v.currentAccount = index | ||
v.config.LastAccountUsed = index | ||
if err := v.save(); err != nil { | ||
return err | ||
} | ||
return v.api.SetAccount(v.config.Accounts[index].PrivKey.String()) | ||
} | ||
|
||
func (v *CensusCLI) getCurrentAccount() *Account { | ||
if v.currentAccount < 0 { | ||
return nil | ||
} | ||
return &v.config.Accounts[v.currentAccount] | ||
} | ||
|
||
func (v *CensusCLI) setAPIaccount(key, memo string) error { | ||
if err := v.api.SetAccount(key); err != nil { | ||
return err | ||
} | ||
// check if already exist to update only memo | ||
key = util.TrimHex(key) | ||
for i, k := range v.config.Accounts { | ||
if k.PrivKey.String() == key { | ||
v.config.Accounts[i].Memo = memo | ||
v.currentAccount = i | ||
return nil | ||
} | ||
} | ||
keyb, err := hex.DecodeString(key) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
signer := ethereum.SignKeys{} | ||
if err := signer.AddHexKey(key); err != nil { | ||
return err | ||
} | ||
|
||
v.config.Accounts = append(v.config.Accounts, | ||
Account{ | ||
PrivKey: keyb, | ||
Address: signer.Address(), | ||
PublicKey: signer.PublicKey(), | ||
Memo: memo, | ||
}) | ||
v.currentAccount = len(v.config.Accounts) - 1 | ||
return v.save() | ||
} | ||
|
||
// listAccounts list the memo notes of all stored accounts | ||
func (v *CensusCLI) listAccounts() []string { | ||
accounts := []string{} | ||
for _, a := range v.config.Accounts { | ||
accounts = append(accounts, a.Memo) | ||
} | ||
return accounts | ||
} | ||
|
||
func (v *CensusCLI) save() error { | ||
return v.config.Save(v.filepath) | ||
} | ||
|
||
func (v *CensusCLI) storeCensus(records [][]string) (*db.Census, error) { | ||
// create the empty census | ||
census := &db.Census{ | ||
CreatedAt: time.Now(), | ||
} | ||
censusID, err := v.db.SetCensus(census) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not create census: %v", err) | ||
} | ||
voters := make([]db.Voter, len(records)) | ||
// Process the records storing first in the DB | ||
|
||
for id, record := range records { | ||
// create random seed | ||
voters[id] = db.Voter{ | ||
OrgID: record[0], | ||
Name: record[1], | ||
Email: record[2], | ||
CensusID: censusID, | ||
Seed: ethereum.HashRaw(util.RandomBytes(32)), | ||
} | ||
log.Debugf("User %v\n", voters[id]) | ||
} | ||
insertedNumber, err := v.db.BulkSetVoters(voters) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not store voters: %v", err) | ||
} | ||
if insertedNumber != len(voters) { | ||
return nil, fmt.Errorf("could not store all voters") | ||
} | ||
|
||
// After voters are stored in the DB create the census | ||
|
||
vocCensus, err := v.api.NewCensus(api.CensusTypeWeighted) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not create census: %v", err) | ||
} | ||
|
||
// add the voters to the census in chunks of 5000 to avoid vochain issues | ||
cparts := api.CensusParticipants{} | ||
for i, voter := range voters { | ||
// extract the voter key | ||
signer := ethereum.SignKeys{} | ||
if err := signer.AddHexKey(hex.EncodeToString(voter.Seed) + v.salt); err != nil { | ||
return nil, fmt.Errorf("could not create signer: %v", err) | ||
} | ||
cparts.Participants = append(cparts.Participants, api.CensusParticipant{ | ||
Key: signer.Address().Bytes(), | ||
Weight: (*types.BigInt)(big.NewInt(1)), | ||
}) | ||
|
||
// if the next voter is in the new chunk upload the chunk and reset list | ||
if (i+1)%5000 == 0 || i == len(voters)-1 { | ||
if err := v.api.CensusAddParticipants(vocCensus, &cparts); err != nil { | ||
return nil, fmt.Errorf("could not add participants: %v", err) | ||
} | ||
cparts = api.CensusParticipants{} | ||
break | ||
} | ||
} | ||
|
||
// upload census | ||
root, uri, err := v.api.CensusPublish(vocCensus) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not publish census: %v", err) | ||
} | ||
|
||
censusSize, err := v.api.CensusSize(vocCensus) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not get census size: %v", err) | ||
} | ||
// if censusSize != uint64(len(voters)) { | ||
// return nil, fmt.Errorf("census size mismatch: %d != %d", censusSize, len(voters)) | ||
// } | ||
|
||
log.Debugf("Census size: %d", censusSize) | ||
|
||
// update the DB census with the ID hash and root | ||
census.VochainID = vocCensus | ||
census.Root = root.String() | ||
census.URI = uri | ||
census.UpdatedAt = time.Now() | ||
|
||
if _, err := v.db.SetCensus(census); err != nil { | ||
return nil, fmt.Errorf("could not update census: %v", err) | ||
} | ||
|
||
return census, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package main | ||
|
||
import ( | ||
"cmp" | ||
"os" | ||
"os/exec" | ||
"strings" | ||
) | ||
|
||
// DefaultEditor is nano because I prefer it. | ||
const DefaultEditor = "nano" | ||
|
||
// PreferredEditorResolver is a function that returns an editor that the user prefers to use, such as the configured | ||
// `$EDITOR` environment variable. | ||
type PreferredEditorResolver func() string | ||
|
||
// GetPreferredEditorFromEnvironment returns the user's editor as defined by the `$EDITOR` environment variable, or | ||
// the `DefaultEditor` if it is not set. | ||
func GetPreferredEditorFromEnvironment() string { | ||
return cmp.Or(os.Getenv("EDITOR"), DefaultEditor) | ||
} | ||
|
||
func resolveEditorArguments(executable, filename string) []string { | ||
args := []string{filename} | ||
|
||
if strings.Contains(executable, "Visual Studio Code.app") { | ||
args = append([]string{"--wait"}, args...) | ||
} | ||
|
||
// Other common editors | ||
// | ||
// ... | ||
// | ||
|
||
return args | ||
} | ||
|
||
// OpenFileInEditor opens filename in a text editor. | ||
func OpenFileInEditor(filename string, resolveEditor PreferredEditorResolver) error { | ||
// Get the full executable path for the editor. | ||
executable, err := exec.LookPath(resolveEditor()) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
cmd := exec.Command(executable, resolveEditorArguments(executable, filename)...) | ||
cmd.Stdin = os.Stdin | ||
cmd.Stdout = os.Stdout | ||
cmd.Stderr = os.Stderr | ||
|
||
return cmd.Run() | ||
} |
Oops, something went wrong.