diff --git a/config.go b/config.go index 24734793..fc77e81c 100644 --- a/config.go +++ b/config.go @@ -67,50 +67,50 @@ var runServiceCommand func(string) error // // See loadConfig for details on the configuration load process. type config struct { - ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` - ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` - DataDir string `short:"b" long:"datadir" description:"Directory to store data"` - LogDir string `long:"logdir" description:"Directory to log output."` - Listen string `long:"listen" description:"Listen for connections on the specified interface/port (default all interfaces port: 9113, testnet: 19113)"` - TestNet bool `long:"testnet" description:"Use the test network"` - SimNet bool `long:"simnet" description:"Use the simulation test network"` - Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` - CPUProfile string `long:"cpuprofile" description:"Write CPU profile to the specified file"` - MemProfile string `long:"memprofile" description:"Write mem profile to the specified file"` - DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify =,=,... to set the log level for individual subsystems -- Use show to list available subsystems"` - APISecret string `long:"apisecret" description:"Secret string used to encrypt API tokens."` - BaseURL string `long:"baseurl" description:"BaseURL to use when sending links via email"` + ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` + ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` + DataDir string `short:"b" long:"datadir" description:"Directory to store data"` + LogDir string `long:"logdir" description:"Directory to log output."` + Listen string `long:"listen" description:"Listen for connections on the specified interface/port (default all interfaces port: 9113, testnet: 19113)"` + TestNet bool `long:"testnet" description:"Use the test network"` + SimNet bool `long:"simnet" description:"Use the simulation test network"` + Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` + CPUProfile string `long:"cpuprofile" description:"Write CPU profile to the specified file"` + MemProfile string `long:"memprofile" description:"Write mem profile to the specified file"` + DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify =,=,... to set the log level for individual subsystems -- Use show to list available subsystems"` + APISecret string `long:"apisecret" description:"Secret string used to encrypt API tokens."` + BaseURL string `long:"baseurl" description:"BaseURL to use when sending links via email"` // todo: can `ColdWalletExtPub` and `PoolFees` be read from stakepoold via rpc? - ColdWalletExtPub string `long:"coldwalletextpub" description:"The extended public key for addresses to which voting service user fees are sent."` - ClosePool bool `long:"closepool" description:"Disable user registration actions (sign-ups and submitting addresses)"` - ClosePoolMsg string `long:"closepoolmsg" description:"Message to display when closepool is set."` - CookieSecret string `long:"cookiesecret" description:"Secret string used to encrypt session data."` - CookieSecure bool `long:"cookiesecure" description:"Set whether cookies can be sent in clear text or not."` - DBHost string `long:"dbhost" description:"Hostname for database connection"` - DBUser string `long:"dbuser" description:"Username for database connection"` - DBPassword string `long:"dbpassword" description:"Password for database connection"` - DBPort string `long:"dbport" description:"Port for database connection"` - DBName string `long:"dbname" description:"Name of database"` - PublicPath string `long:"publicpath" description:"Path to the public folder which contains css/fonts/images/javascript."` - TemplatePath string `long:"templatepath" description:"Path to the views folder which contains html files."` - PoolEmail string `long:"poolemail" description:"Email address to for support inquiries"` - PoolFees float64 `long:"poolfees" description:"The per-ticket fees the user must send to the pool with their tickets"` - PoolLink string `long:"poollink" description:"URL for support inquiries such as forum, IRC, etc"` - RealIPHeader string `long:"realipheader" description:"The name of an HTTP request header containing the actual remote client IP address, typically set by a reverse proxy. An empty string (default) indicates to use net/Request.RemodeAddr."` - SMTPFrom string `long:"smtpfrom" description:"From address to use on outbound mail"` - SMTPHost string `long:"smtphost" description:"SMTP hostname/ip and port, e.g. mail.example.com:25"` - SMTPUsername string `long:"smtpusername" description:"SMTP username for authentication if required"` - SMTPPassword string `long:"smtppassword" description:"SMTP password for authentication if required"` - UseSMTPS bool `long:"usesmtps" description:"Connect to the SMTP server using smtps."` - SMTPSkipVerify bool `long:"smtpskipverify" description:"Skip SMTP TLS cert verification. Will only skip if SMTPCert is empty"` - SMTPCert string `long:"smtpcert" description:"Path for the smtp certificate file"` - SystemCerts *x509.CertPool - StakepooldHosts []string `long:"stakepooldhosts" description:"Hostnames for stakepoold servers"` - StakepooldCerts []string `long:"stakepooldcerts" description:"Certificate paths for stakepoold servers"` - WalletHosts []string `long:"wallethosts" description:"Deprecated: dcrstakepool no longer connects to dcrwallet"` - WalletUsers []string `long:"walletusers" description:"Deprecated: dcrstakepool no longer connects to dcrwallet"` - WalletPasswords []string `long:"walletpasswords" description:"Deprecated: dcrstakepool no longer connects to dcrwallet"` - WalletCerts []string `long:"walletcerts" description:"Deprecated: dcrstakepool no longer connects to dcrwallet"` + ColdWalletExtPub string `long:"coldwalletextpub" description:"The extended public key for addresses to which voting service user fees are sent."` + ClosePool bool `long:"closepool" description:"Disable user registration actions (sign-ups and submitting addresses)"` + ClosePoolMsg string `long:"closepoolmsg" description:"Message to display when closepool is set."` + CookieSecret string `long:"cookiesecret" description:"Secret string used to encrypt session data."` + CookieSecure bool `long:"cookiesecure" description:"Set whether cookies can be sent in clear text or not."` + DBHost string `long:"dbhost" description:"Hostname for database connection"` + DBUser string `long:"dbuser" description:"Username for database connection"` + DBPassword string `long:"dbpassword" description:"Password for database connection"` + DBPort string `long:"dbport" description:"Port for database connection"` + DBName string `long:"dbname" description:"Name of database"` + PublicPath string `long:"publicpath" description:"Path to the public folder which contains css/fonts/images/javascript."` + TemplatePath string `long:"templatepath" description:"Path to the views folder which contains html files."` + PoolEmail string `long:"poolemail" description:"Email address to for support inquiries"` + PoolFees float64 `long:"poolfees" description:"The per-ticket fees the user must send to the pool with their tickets"` + PoolLink string `long:"poollink" description:"URL for support inquiries such as forum, IRC, etc"` + RealIPHeader string `long:"realipheader" description:"The name of an HTTP request header containing the actual remote client IP address, typically set by a reverse proxy. An empty string (default) indicates to use net/Request.RemodeAddr."` + SMTPFrom string `long:"smtpfrom" description:"From address to use on outbound mail"` + SMTPHost string `long:"smtphost" description:"SMTP hostname/ip and port, e.g. mail.example.com:25"` + SMTPUsername string `long:"smtpusername" description:"SMTP username for authentication if required"` + SMTPPassword string `long:"smtppassword" description:"SMTP password for authentication if required"` + UseSMTPS bool `long:"usesmtps" description:"Connect to the SMTP server using smtps."` + SMTPSkipVerify bool `long:"smtpskipverify" description:"Skip SMTP TLS cert verification. Will only skip if SMTPCert is empty"` + SMTPCert string `long:"smtpcert" description:"Path for the smtp certificate file"` + SystemCerts *x509.CertPool + StakepooldHosts []string `long:"stakepooldhosts" description:"Hostnames for stakepoold servers"` + StakepooldCerts []string `long:"stakepooldcerts" description:"Certificate paths for stakepoold servers"` + WalletHosts []string `long:"wallethosts" description:"Deprecated: dcrstakepool no longer connects to dcrwallet"` + WalletUsers []string `long:"walletusers" description:"Deprecated: dcrstakepool no longer connects to dcrwallet"` + WalletPasswords []string `long:"walletpasswords" description:"Deprecated: dcrstakepool no longer connects to dcrwallet"` + WalletCerts []string `long:"walletcerts" description:"Deprecated: dcrstakepool no longer connects to dcrwallet"` // todo: `VotingWalletExtPub` can be read from the vsp backend dcrwallet via stakepoold rpc instead! VotingWalletExtPub string `long:"votingwalletextpub" description:"The extended public key of the default account of the voting wallet"` AdminIPs []string `long:"adminips" description:"Expected admin host"` diff --git a/controllers/main.go b/controllers/main.go index 53f44fec..73608d39 100644 --- a/controllers/main.go +++ b/controllers/main.go @@ -280,6 +280,8 @@ func (controller *MainController) API(c web.C, r *http.Request) *system.APIRespo switch command { case "address": _, code, response, err = controller.APIAddress(c, r) + case "purchaseticket": + data, code, response, err = controller.APIPurchaseTicket(c, r) case "voting": _, code, response, err = controller.APIVoting(c, r) default: @@ -318,7 +320,7 @@ func (controller *MainController) APIAddress(c web.C, r *http.Request) ([]string } // Get the ticket address for this user - pooladdress, err := controller.TicketAddressForUserID(int(c.Env["APIUserID"].(int64))) + pooladdress, err := controller.TicketAddressForUserID(int(user.Id)) if err != nil { log.Errorf("unable to derive ticket address: %v", err) return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands") @@ -356,6 +358,7 @@ func (controller *MainController) APIAddress(c web.C, r *http.Request) ([]string var importedHeight int64 importedHeight, err = controller.StakepooldServers.ImportScript(serializedScript) if err != nil { + log.Warnf("unexpected error importing multisig redeem script: %v", err) return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands") } @@ -369,7 +372,7 @@ func (controller *MainController) APIAddress(c web.C, r *http.Request) ([]string createMultiSig.RedeemScript, poolPubKeyAddr, userPubKeyAddr, userFeeAddr.EncodeAddress(), importedHeight) - log.Infof("successfully create multisigaddress for user %d", c.Env["APIUserID"]) + log.Infof("successfully create multisigaddress for user %d", int(user.Id)) err = controller.StakepooldUpdateUsers(dbMap) if err != nil { @@ -379,6 +382,120 @@ func (controller *MainController) APIAddress(c web.C, r *http.Request) ([]string return nil, codes.OK, "address successfully imported", nil } +// APIPurchaseTicket returns relevant vsp information to enable clients purchase tickets that this vsp can vote on +func (controller *MainController) APIPurchaseTicket(c web.C, r *http.Request) (*poolapi.PurchaseInfo, codes.Code, string, error) { + userPubKeyAddr := r.FormValue("UserPubKeyAddr") + + if _, err := validateUserPubKeyAddr(userPubKeyAddr); err != nil { + return nil, codes.InvalidArgument, "address error", err + } + + dbMap := controller.GetDbMap(c) + + // check if this userPubKeyAddr has been used previously to generate ticket purchase parameters + user := models.GetUserByEmail(dbMap, userPubKeyAddr) + if user != nil { + purchaseInfo := &poolapi.PurchaseInfo{ + PoolAddress: user.UserFeeAddr, + PoolFees: controller.poolFees, + Script: user.MultiSigScript, + TicketAddress: user.MultiSigAddress, + VoteBits: uint16(user.VoteBits), + } + return purchaseInfo, codes.OK, "APIPurchaseTicket: purchaseinfo retrieved for existing userPubKeyAddr", nil + } + + user = &models.User{ + Username: userPubKeyAddr, + Email: userPubKeyAddr, + EmailVerified: 0, + VoteBits: 1, + VoteBitsVersion: int64(controller.voteVersion), + } + + remoteIP := getClientIP(r, controller.realIPHeader) + log.Infof("APIPurchaseTicket POST from %v, created new user account %v. Inserting.", remoteIP, user.Email) + + err := models.InsertUser(dbMap, user) + if err != nil { + log.Errorf("Error while creating new user account for ticket purchase: %v", err) + return nil, codes.Unavailable, "system error", errors.New("unable to setup db entry for purchase") + } + + // load saved user info from db to get the user id + user = models.GetUserByEmail(dbMap, userPubKeyAddr) + userId := int(user.Id) + + // Get the ticket address for this user + pooladdress, err := controller.TicketAddressForUserID(int(user.Id)) + if err != nil { + log.Errorf("unable to derive ticket address: %v", err) + return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands") + } + + poolValidateAddress, err := controller.StakepooldServers.ValidateAddress(pooladdress) + if err != nil { + log.Errorf("unable to validate address: %v", err) + return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands") + } + if !poolValidateAddress.IsMine { + log.Errorf("unable to validate ismine for pool ticket address: %s", + pooladdress.String()) + return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands") + } + + poolPubKeyAddr := poolValidateAddress.PubKeyAddr + + if _, err = dcrutil.DecodeAddress(poolPubKeyAddr); err != nil { + return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands") + } + + createMultiSig, err := controller.StakepooldServers.CreateMultisig([]string{poolPubKeyAddr, userPubKeyAddr}) + if err != nil { + return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands") + } + + // Serialize the redeem script (hex string -> []byte) + serializedScript, err := hex.DecodeString(createMultiSig.RedeemScript) + if err != nil { + return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands") + } + + // Import the redeem script + var importedHeight int64 + importedHeight, err = controller.StakepooldServers.ImportScript(serializedScript) + if err != nil { + log.Warnf("unexpected error importing multisig redeem script: %v", err) + return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands") + } + + userFeeAddr, err := controller.FeeAddressForUserID(int(user.Id)) + if err != nil { + log.Warnf("unexpected error deriving pool addr: %s", err.Error()) + return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands") + } + + models.UpdateUserByID(dbMap, user.Id, createMultiSig.Address, + createMultiSig.RedeemScript, poolPubKeyAddr, userPubKeyAddr, + userFeeAddr.EncodeAddress(), importedHeight) + + log.Infof("successfully create multisigaddress for user %d", userId) + + err = controller.StakepooldUpdateUsers(dbMap) + if err != nil { + log.Warnf("failure to update users: %v", err) + } + + purchaseInfo := &poolapi.PurchaseInfo{ + PoolAddress: userFeeAddr.EncodeAddress(), + PoolFees: controller.poolFees, + Script: createMultiSig.RedeemScript, + TicketAddress: createMultiSig.Address, + VoteBits: uint16(user.VoteBits), + } + return purchaseInfo, codes.OK, "APIPurchaseTicket: purchaseinfo generated for userPubKeyAddr", nil +} + // APIPurchaseInfo fetches and returns the user's info or an error func (controller *MainController) APIPurchaseInfo(c web.C, r *http.Request) (*poolapi.PurchaseInfo, codes.Code, string, error) {