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 for accountless ticket purchase #515

Closed
Closed
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
192 changes: 103 additions & 89 deletions controllers/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,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 @@ -303,66 +305,64 @@ func (controller *MainController) APIAddress(c web.C, r *http.Request) ([]string
return nil, codes.InvalidArgument, "address error", err
}

// Get the ticket address for this user
pooladdress, err := controller.TicketAddressForUserID(int(c.Env["APIUserID"].(int64)))
if err != nil {
log.Errorf("unable to derive ticket address: %v", err)
if _, err := controller.generateTicketPurchaseInfo(r.Context(), dbMap, user, userPubKeyAddr); err != nil {
log.Errorf("generateTicketPurchaseInfo: %v", err)
return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands")
}

poolValidateAddress, err := controller.Cfg.StakepooldServers.ValidateAddress(r.Context(), 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")
}
return nil, codes.OK, "address successfully imported", nil
}

poolPubKeyAddr := poolValidateAddress.PubKeyAddr
// 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 = dcrutil.DecodeAddress(poolPubKeyAddr, controller.Cfg.NetParams); err != nil {
return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands")
if _, err := validateUserPubKeyAddr(userPubKeyAddr, controller.Cfg.NetParams); err != nil {
return nil, codes.InvalidArgument, "address error", err
}

createMultiSig, err := controller.Cfg.StakepooldServers.CreateMultisig(r.Context(), []string{poolPubKeyAddr, userPubKeyAddr})
if err != nil {
return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands")
}
dbMap := controller.GetDbMap(c)

// 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")
// 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.Cfg.PoolFees,
Script: user.MultiSigScript,
TicketAddress: user.MultiSigAddress,
VoteBits: uint16(user.VoteBits),
}
return purchaseInfo, codes.OK, "APIPurchaseTicket: purchaseinfo retrieved for existing userPubKeyAddr", nil
}

// Import the redeem script
var importedHeight int64
importedHeight, err = controller.Cfg.StakepooldServers.ImportNewScript(r.Context(), serializedScript)
if err != nil {
return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands")
user = &models.User{
Username: userPubKeyAddr,
Email: userPubKeyAddr,
EmailVerified: 0,
VoteBits: 1,
VoteBitsVersion: int64(controller.voteVersion),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it would be best to put in some random password? Just to be safe and sure noone can access this account through http...

}

userFeeAddr, err := controller.FeeAddressForUserID(int(user.ID))
remoteIP := getClientIP(r, controller.Cfg.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.Warnf("unexpected error deriving pool addr: %s", err.Error())
return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands")
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")
}

models.UpdateUserByID(dbMap, user.ID, createMultiSig.Address,
createMultiSig.RedeemScript, poolPubKeyAddr, userPubKeyAddr,
userFeeAddr.Address(), importedHeight)

log.Infof("successfully create multisigaddress for user %d", c.Env["APIUserID"])
// load saved user info from db to get the user id
user = models.GetUserByEmail(dbMap, userPubKeyAddr)

err = controller.StakepooldUpdateUsers(r.Context(), dbMap)
purchaseInfo, err := controller.generateTicketPurchaseInfo(r.Context(), dbMap, user, userPubKeyAddr)
if err != nil {
log.Warnf("failure to update users: %v", err)
log.Errorf("generateTicketPurchaseInfo: %v", err)
return nil, codes.Unavailable, "system error", errors.New("unable to process wallet commands")
}

return nil, codes.OK, "address successfully imported", nil
return purchaseInfo, codes.OK, "APIPurchaseTicket: purchaseinfo generated for userPubKeyAddr", nil
}

// APIPurchaseInfo fetches and returns the user's info or an error
Expand Down Expand Up @@ -748,97 +748,111 @@ func validateUserPubKeyAddr(pubKeyAddr string, params *chaincfg.Params) (dcrutil
return u, nil
}

// AddressPost is address form submit route.
func (controller *MainController) AddressPost(c web.C, r *http.Request) (string, int) {
session := controller.GetSession(c)
c.Env[csrf.TemplateTag] = csrf.TemplateField(r)
remoteIP := getClientIP(r, controller.Cfg.RealIPHeader)

if session.Values["UserId"] == nil {
return "/", http.StatusSeeOther
}
uid64 := session.Values["UserId"].(int64)

// Only accept address if user does not already have a PubKeyAddr set.
dbMap := controller.GetDbMap(c)
user, _ := models.GetUserByID(dbMap, session.Values["UserId"].(int64))
if len(user.UserPubKeyAddr) > 0 {
session.AddFlash("The voting service is currently limited to one address per account", "address")
return controller.Address(c, r)
}

userPubKeyAddr := r.FormValue("UserPubKeyAddr")

log.Infof("Address POST from %v, pubkeyaddr %v", remoteIP, userPubKeyAddr)

if _, err := validateUserPubKeyAddr(userPubKeyAddr, controller.Cfg.NetParams); err != nil {
session.AddFlash(err.Error(), "address")
return controller.Address(c, r)
}
func (controller *MainController) generateTicketPurchaseInfo(ctx context.Context, dbMap *gorp.DbMap,
user *models.User, userPubKeyAddr string) (*poolapi.PurchaseInfo, error) {

// Get the ticket address for this user
pooladdress, err := controller.TicketAddressForUserID(int(uid64))
pooladdress, err := controller.TicketAddressForUserID(int(user.ID))
if err != nil {
log.Errorf("unable to derive ticket address: %v", err)
session.AddFlash("Unable to derive ticket address", "address")
return controller.Address(c, r)
return nil, fmt.Errorf("unable to derive ticket address: %v", err)
}

// From new address (pkh), get pubkey address
poolValidateAddress, err := controller.Cfg.StakepooldServers.ValidateAddress(r.Context(), pooladdress)
poolValidateAddress, err := controller.Cfg.StakepooldServers.ValidateAddress(ctx, pooladdress)
if err != nil {
return "/error", http.StatusSeeOther
return nil, fmt.Errorf("unable to validate address: %v", err)
}
if !poolValidateAddress.IsMine {
log.Errorf("unable to validate ismine for pool ticket address: %s",
return nil, fmt.Errorf("unable to validate ismine for pool ticket address: %s",
pooladdress.String())
session.AddFlash("Unable to validate pool ticket address", "address")
return controller.Address(c, r)
}

poolPubKeyAddr := poolValidateAddress.PubKeyAddr

// Get back Address from pool's new pubkey address
if _, err = dcrutil.DecodeAddress(poolPubKeyAddr, controller.Cfg.NetParams); err != nil {
return "/error", http.StatusSeeOther
return nil, fmt.Errorf("unable to decode pool address: %v", err)
}

// Create the the multisig script. Result includes a P2SH and redeem script.
createMultiSig, err := controller.Cfg.StakepooldServers.CreateMultisig(r.Context(), []string{poolPubKeyAddr, userPubKeyAddr})
createMultiSig, err := controller.Cfg.StakepooldServers.CreateMultisig(ctx, []string{poolPubKeyAddr, userPubKeyAddr})
if err != nil {
return "/error", http.StatusSeeOther
return nil, fmt.Errorf("create multisig error: %v", err)
}

// Serialize the redeem script (hex string -> []byte)
serializedScript, err := hex.DecodeString(createMultiSig.RedeemScript)
if err != nil {
return "/error", http.StatusSeeOther
return nil, fmt.Errorf("unable to serialize redeem script hex: %v", err)
}

// Import the redeem script
var importedHeight int64
importedHeight, err = controller.Cfg.StakepooldServers.ImportNewScript(r.Context(), serializedScript)
importedHeight, err = controller.Cfg.StakepooldServers.ImportNewScript(ctx, serializedScript)
if err != nil {
return "/error", http.StatusSeeOther
return nil, fmt.Errorf("unexpected error importing multisig redeem script: %v", err)
}

// Get the pool fees address for this user
userFeeAddr, err := controller.FeeAddressForUserID(int(uid64))
userFeeAddr, err := controller.FeeAddressForUserID(int(user.ID))
if err != nil {
log.Errorf("unexpected error deriving fee addr: %s", err.Error())
session.AddFlash("Unable to derive fee address", "address")
return controller.Address(c, r)
return nil, fmt.Errorf("unexpected error deriving fee addr: %s", err.Error())
}

// Update the user's DB entry with multisig, user and pool pubkey
// addresses, and the fee address
models.UpdateUserByID(dbMap, uid64, createMultiSig.Address,
models.UpdateUserByID(dbMap, user.ID, createMultiSig.Address,
createMultiSig.RedeemScript, poolPubKeyAddr, userPubKeyAddr,
userFeeAddr.Address(), importedHeight)

if err = controller.StakepooldUpdateUsers(r.Context(), dbMap); err != nil {
log.Infof("successfully create multisigaddress for user %d", int(user.ID))

if err = controller.StakepooldUpdateUsers(ctx, dbMap); err != nil {
log.Errorf("unable to update all: %v", err)
}

return &poolapi.PurchaseInfo{
PoolAddress: userFeeAddr.Address(),
PoolFees: controller.Cfg.PoolFees,
Script: createMultiSig.RedeemScript,
TicketAddress: createMultiSig.Address,
VoteBits: uint16(user.VoteBits),
}, nil
}

// AddressPost is address form submit route.
func (controller *MainController) AddressPost(c web.C, r *http.Request) (string, int) {
session := controller.GetSession(c)
c.Env[csrf.TemplateTag] = csrf.TemplateField(r)
remoteIP := getClientIP(r, controller.Cfg.RealIPHeader)

if session.Values["UserId"] == nil {
return "/", http.StatusSeeOther
}
uid64 := session.Values["UserId"].(int64)

// Only accept address if user does not already have a PubKeyAddr set.
dbMap := controller.GetDbMap(c)
user, _ := models.GetUserByID(dbMap, uid64)
if len(user.UserPubKeyAddr) > 0 {
session.AddFlash("The voting service is currently limited to one address per account", "address")
return controller.Address(c, r)
}

userPubKeyAddr := r.FormValue("UserPubKeyAddr")

log.Infof("Address POST from %v, pubkeyaddr %v", remoteIP, userPubKeyAddr)

if _, err := validateUserPubKeyAddr(userPubKeyAddr, controller.Cfg.NetParams); err != nil {
session.AddFlash(err.Error(), "address")
return controller.Address(c, r)
}

if _, err := controller.generateTicketPurchaseInfo(r.Context(), dbMap, user, userPubKeyAddr); err != nil {
log.Errorf("generateTicketPurchaseInfo: %v", err)
return "/error", http.StatusSeeOther
}

return "/tickets", http.StatusSeeOther
}

Expand Down