Skip to content

Commit

Permalink
Feat: Implements census parse and mailing cli
Browse files Browse the repository at this point in the history
* Adds cli logic
* Adds census and voter collections to MongoDB storage
  • Loading branch information
emmdim committed Feb 5, 2025
1 parent 921755c commit 89f6f6d
Show file tree
Hide file tree
Showing 10 changed files with 836 additions and 3 deletions.
270 changes: 270 additions & 0 deletions cmd/cli/censuscli.go
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
}
52 changes: 52 additions & 0 deletions cmd/cli/editor.go
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()
}
Loading

0 comments on commit 89f6f6d

Please sign in to comment.