github.com/decred/politeia@v1.4.0/politeiawww/legacy/piuser.go (about) 1 // Copyright (c) 2017-2020 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package legacy 6 7 import ( 8 "context" 9 "fmt" 10 "sort" 11 "time" 12 13 www "github.com/decred/politeia/politeiawww/api/www/v1" 14 "github.com/decred/politeia/politeiawww/legacy/user" 15 ) 16 17 // processUserRegistrationPayment verifies that the provided transaction 18 // meets the minimum requirements to mark the user as paid, and then does 19 // that in the user database. 20 func (p *Politeiawww) processUserRegistrationPayment(ctx context.Context, u *user.User) (*www.UserRegistrationPaymentReply, error) { 21 var reply www.UserRegistrationPaymentReply 22 if p.userHasPaid(*u) { 23 reply.HasPaid = true 24 return &reply, nil 25 } 26 27 if paywallHasExpired(u.NewUserPaywallPollExpiry) { 28 err := p.generateNewUserPaywall(u) 29 if err != nil { 30 return nil, err 31 } 32 reply.PaywallAddress = u.NewUserPaywallAddress 33 reply.PaywallAmount = u.NewUserPaywallAmount 34 reply.PaywallTxNotBefore = u.NewUserPaywallTxNotBefore 35 return &reply, nil 36 } 37 38 tx, _, err := fetchTxWithBlockExplorers(ctx, p.params, 39 u.NewUserPaywallAddress, u.NewUserPaywallAmount, 40 u.NewUserPaywallTxNotBefore, p.cfg.MinConfirmationsRequired, 41 p.dcrdataHostHTTP()) 42 if err != nil { 43 return nil, err 44 } 45 46 if tx != "" { 47 reply.HasPaid = true 48 49 err = p.updateUserAsPaid(u, tx) 50 if err != nil { 51 return nil, err 52 } 53 } else { 54 // TODO: Add the user to the in-memory pool. 55 } 56 57 return &reply, nil 58 } 59 60 // processUserProposalPaywall returns a proposal paywall that enables the 61 // the user to purchase proposal credits. The user can only have one paywall 62 // active at a time. If no paywall currently exists, a new one is created and 63 // the user is added to the paywall pool. 64 func (p *Politeiawww) processUserProposalPaywall(u *user.User) (*www.UserProposalPaywallReply, error) { 65 log.Tracef("processUserProposalPaywall") 66 67 // Ensure paywall is enabled 68 if !p.paywallIsEnabled() { 69 return &www.UserProposalPaywallReply{}, nil 70 } 71 72 // Proposal paywalls cannot be generated until the user has paid their 73 // user registration fee. 74 if !p.userHasPaid(*u) { 75 return nil, www.UserError{ 76 ErrorCode: www.ErrorStatusUserNotPaid, 77 } 78 } 79 80 var pp *user.ProposalPaywall 81 if p.userHasValidProposalPaywall(u) { 82 // Don't create a new paywall if a valid one already exists. 83 pp = p.mostRecentProposalPaywall(u) 84 } else { 85 // Create a new paywall. 86 var err error 87 pp, err = p.generateProposalPaywall(u) 88 if err != nil { 89 return nil, err 90 } 91 } 92 93 return &www.UserProposalPaywallReply{ 94 CreditPrice: pp.CreditPrice, 95 PaywallAddress: pp.Address, 96 PaywallTxNotBefore: pp.TxNotBefore, 97 }, nil 98 } 99 100 // processUserProposalPaywallTx checks if the user has a pending paywall 101 // payment and returns the payment details if one is found. 102 func (p *Politeiawww) processUserProposalPaywallTx(u *user.User) (*www.UserProposalPaywallTxReply, error) { 103 log.Tracef("processUserProposalPaywallTx") 104 105 var ( 106 txID string 107 txAmount uint64 108 confirmations uint64 109 ) 110 111 p.RLock() 112 defer p.RUnlock() 113 114 poolMember, ok := p.userPaywallPool[u.ID] 115 if ok { 116 txID = poolMember.txID 117 txAmount = poolMember.txAmount 118 confirmations = poolMember.txConfirmations 119 } 120 121 return &www.UserProposalPaywallTxReply{ 122 TxID: txID, 123 TxAmount: txAmount, 124 Confirmations: confirmations, 125 }, nil 126 } 127 128 // processUserProposalCredits returns a list of the user's unspent proposal 129 // credits and a list of the user's spent proposal credits. 130 func (p *Politeiawww) processUserProposalCredits(u *user.User) (*www.UserProposalCreditsReply, error) { 131 // Convert from database proposal credits to www proposal credits. 132 upc := make([]www.ProposalCredit, len(u.UnspentProposalCredits)) 133 for i, credit := range u.UnspentProposalCredits { 134 upc[i] = convertProposalCreditFromUserDB(credit) 135 } 136 spc := make([]www.ProposalCredit, len(u.SpentProposalCredits)) 137 for i, credit := range u.SpentProposalCredits { 138 spc[i] = convertProposalCreditFromUserDB(credit) 139 } 140 141 return &www.UserProposalCreditsReply{ 142 UnspentCredits: upc, 143 SpentCredits: spc, 144 }, nil 145 } 146 147 // processUserPaymentsRescan allows an admin to rescan a user's paywall address 148 // to check for any payments that may have been missed by paywall polling. 149 func (p *Politeiawww) processUserPaymentsRescan(ctx context.Context, upr www.UserPaymentsRescan) (*www.UserPaymentsRescanReply, error) { 150 // Ensure paywall is enabled 151 if !p.paywallIsEnabled() { 152 return &www.UserPaymentsRescanReply{}, nil 153 } 154 155 // Lookup user 156 u, err := p.userByIDStr(upr.UserID) 157 if err != nil { 158 return nil, err 159 } 160 161 // Fetch user payments 162 payments, err := fetchTxsForAddressNotBefore(ctx, p.params, 163 u.NewUserPaywallAddress, u.NewUserPaywallTxNotBefore, 164 p.dcrdataHostHTTP()) 165 if err != nil { 166 return nil, fmt.Errorf("FetchTxsForAddressNotBefore: %v", err) 167 } 168 169 // Paywalls are in chronological order so sort txs into chronological 170 // order to make them easier to work with 171 sort.SliceStable(payments, func(i, j int) bool { 172 return payments[i].Timestamp < payments[j].Timestamp 173 }) 174 175 // Sanity check. Paywalls should already be in chronological order. 176 paywalls := u.ProposalPaywalls 177 sort.SliceStable(paywalls, func(i, j int) bool { 178 return paywalls[i].TxNotBefore < paywalls[j].TxNotBefore 179 }) 180 181 // Check for payments that were missed by paywall polling 182 newCredits := make([]user.ProposalCredit, 0, len(payments)) 183 for _, payment := range payments { 184 // Check if the payment transaction corresponds to a user 185 // registration payment. A user registration payment may not 186 // exist if the registration paywall was cleared by an admin. 187 if payment.TxID == u.NewUserPaywallTx { 188 continue 189 } 190 191 // Check for credits that correspond to the payment. If a 192 // credit is found it means that this payment was not missed by 193 // paywall polling and we can continue onto the next payment. 194 var found bool 195 for _, credit := range u.SpentProposalCredits { 196 if credit.TxID == payment.TxID { 197 found = true 198 break 199 } 200 } 201 if found { 202 continue 203 } 204 205 for _, credit := range u.UnspentProposalCredits { 206 if credit.TxID == payment.TxID { 207 found = true 208 break 209 } 210 } 211 if found { 212 continue 213 } 214 215 // Credits were not found for this payment which means that it 216 // was missed by paywall polling. Create new credits using the 217 // paywall details that correspond to the payment timestamp. If 218 // a paywall had not yet been issued, use the current proposal 219 // credit price. 220 var pp user.ProposalPaywall 221 for _, paywall := range paywalls { 222 if payment.Timestamp < paywall.TxNotBefore { 223 continue 224 } 225 if payment.Timestamp > paywall.TxNotBefore { 226 // Corresponding paywall found 227 pp = paywall 228 break 229 } 230 } 231 232 if pp == (user.ProposalPaywall{}) { 233 // Paywall not found. This means the tx occurred before 234 // any paywalls were issued. Use current credit price. 235 pp.CreditPrice = p.cfg.PaywallAmount 236 } 237 238 // Don't add credits if the paywall is in the paywall pool 239 if pp.TxID == "" && !paywallHasExpired(pp.PollExpiry) { 240 continue 241 } 242 243 // Ensure payment has minimum number of confirmations 244 if payment.Confirmations < p.cfg.MinConfirmationsRequired { 245 continue 246 } 247 248 // Create proposal credits 249 numCredits := payment.Amount / pp.CreditPrice 250 c := make([]user.ProposalCredit, numCredits) 251 for i := uint64(0); i < numCredits; i++ { 252 c[i] = user.ProposalCredit{ 253 PaywallID: pp.ID, 254 Price: pp.CreditPrice, 255 DatePurchased: time.Now().Unix(), 256 TxID: payment.TxID, 257 } 258 } 259 newCredits = append(newCredits, c...) 260 } 261 262 // Update user record 263 // We relookup the user record here in case the user has spent proposal 264 // credits since the start of this request. Failure to relookup the 265 // user record here could result in adding proposal credits to the 266 // user's account that have already been spent. 267 u, err = p.userByEmail(u.Email) 268 if err != nil { 269 return nil, fmt.Errorf("UserGet %v", err) 270 } 271 272 u.UnspentProposalCredits = append(u.UnspentProposalCredits, 273 newCredits...) 274 275 err = p.db.UserUpdate(*u) 276 if err != nil { 277 return nil, fmt.Errorf("UserUpdate %v", err) 278 } 279 280 // Convert database credits to www credits 281 newCreditsWWW := make([]www.ProposalCredit, len(newCredits)) 282 for i, credit := range newCredits { 283 newCreditsWWW[i] = convertProposalCreditFromUserDB(credit) 284 } 285 286 return &www.UserPaymentsRescanReply{ 287 NewCredits: newCreditsWWW, 288 }, nil 289 } 290 291 func convertProposalCreditFromUserDB(credit user.ProposalCredit) www.ProposalCredit { 292 return www.ProposalCredit{ 293 PaywallID: credit.PaywallID, 294 Price: credit.Price, 295 DatePurchased: credit.DatePurchased, 296 TxID: credit.TxID, 297 } 298 }