Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add /purchaseticket API endpoint #6

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 43 additions & 43 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <subsystem>=<level>,<subsystem2>=<level>,... 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 <subsystem>=<level>,<subsystem2>=<level>,... 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"`
Expand Down
121 changes: 119 additions & 2 deletions controllers/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
}

Expand All @@ -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 {
Expand All @@ -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) {
Expand Down