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  }