github.com/decred/politeia@v1.4.0/politeiawww/legacy/cmsaddresswatcher.go (about)

     1  // Copyright (c) 2019-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  	"encoding/hex"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"net/http"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/decred/dcrd/dcrutil/v3"
    18  	pstypes "github.com/decred/dcrdata/v6/pubsub/types"
    19  	pd "github.com/decred/politeia/politeiad/api/v1"
    20  	cms "github.com/decred/politeia/politeiawww/api/cms/v1"
    21  	www "github.com/decred/politeia/politeiawww/api/www/v1"
    22  	database "github.com/decred/politeia/politeiawww/legacy/cmsdatabase"
    23  	"github.com/decred/politeia/politeiawww/legacy/mdstream"
    24  	"github.com/decred/politeia/politeiawww/legacy/user"
    25  	"github.com/decred/politeia/politeiawww/wsdcrdata"
    26  	"github.com/decred/politeia/util"
    27  	"github.com/google/uuid"
    28  )
    29  
    30  const (
    31  	// mainnetSubsidyAddr is the mainnet address in which cms payments
    32  	// must come from in order to be considered a valid payment.
    33  	mainnetSubsidyAddr = "Dcur2mcGjmENx4DhNqDctW5wJCVyT3Qeqkx"
    34  )
    35  
    36  func (p *Politeiawww) addWatchAddress(address string) {
    37  	err := p.wsDcrdata.AddressSubscribe(address)
    38  	if err != nil {
    39  		log.Errorf("addWatchAddress: subscribe '%v': %v",
    40  			address, err)
    41  		return
    42  	}
    43  	log.Infof("Subscribed to listen: %v", address)
    44  }
    45  
    46  func (p *Politeiawww) removeWatchAddress(address string) {
    47  	err := p.wsDcrdata.AddressUnsubscribe(address)
    48  	if err != nil {
    49  		log.Errorf("removeWatchAddress: unsubscribe '%v': %v",
    50  			address, err)
    51  		return
    52  	}
    53  	log.Infof("Unsubscribed: %v", address)
    54  }
    55  
    56  func (p *Politeiawww) monitorCMSAddressWatcher(ctx context.Context) {
    57  	defer func() {
    58  		log.Infof("Dcrdata websocket closed")
    59  	}()
    60  
    61  	// Setup messages channel
    62  	receiver := p.wsDcrdata.Receive()
    63  
    64  	for {
    65  		// Monitor for a new message
    66  		msg, ok := <-receiver
    67  		if !ok {
    68  			// Check if the websocket was shut down intentionally or was
    69  			// dropped unexpectedly.
    70  			if p.wsDcrdata.Status() == wsdcrdata.StatusShutdown {
    71  				return
    72  			}
    73  			log.Infof("Dcrdata websocket connection unexpectedly dropped")
    74  			goto reconnect
    75  		}
    76  
    77  		// Handle new message
    78  		switch m := msg.Message.(type) {
    79  		case *pstypes.AddressMessage:
    80  			log.Debugf("Message (%s): AddressMessage(address=%s, txHash=%s)",
    81  				msg.EventId, m.Address, m.TxHash)
    82  
    83  			// Check payment history for address
    84  			payment, err := p.cmsDB.PaymentsByAddress(m.Address)
    85  			if err != nil {
    86  				log.Errorf("error retreiving payments information from db %v", err)
    87  				continue
    88  			}
    89  			if payment.Address != m.Address {
    90  				log.Errorf("payment address does not match watched address message!")
    91  				continue
    92  			}
    93  			paid := p.checkPayments(ctx, payment, m.TxHash)
    94  			if paid {
    95  				p.removeWatchAddress(payment.Address)
    96  			}
    97  		case *pstypes.HangUp:
    98  			log.Infof("Dcrdata websocket has hung up. Will reconnect.")
    99  			goto reconnect
   100  
   101  		case int:
   102  			// Ping messages are of type int
   103  
   104  		default:
   105  			log.Errorf("wsDcrdata message of type %v unhandled: %v",
   106  				msg.EventId, m)
   107  		}
   108  
   109  		// Check for next message
   110  		continue
   111  
   112  	reconnect:
   113  		// Connection was closed for some reason. Reconnect.
   114  		p.wsDcrdata.Reconnect()
   115  
   116  		// Setup a new messages channel using the new connection.
   117  		receiver = p.wsDcrdata.Receive()
   118  
   119  		log.Infof("Successfully reconnected dcrdata websocket")
   120  	}
   121  }
   122  
   123  func (p *Politeiawww) setupCMSAddressWatcher() {
   124  	ctx := context.Background()
   125  
   126  	// Ensure connection is open. If connection is closed, establish a
   127  	// new connection before continuing.
   128  	if p.wsDcrdata.Status() != wsdcrdata.StatusOpen {
   129  		p.wsDcrdata.Reconnect()
   130  	}
   131  
   132  	err := p.restartCMSAddressesWatching(ctx)
   133  	if err != nil {
   134  		log.Errorf("error restarting address watcher %v", err)
   135  		return
   136  	}
   137  
   138  	// Monitor websocket connection in a new go routine
   139  	go p.monitorCMSAddressWatcher(ctx)
   140  
   141  }
   142  func (p *Politeiawww) restartCMSAddressesWatching(ctx context.Context) error {
   143  	approvedInvoices, err := p.cmsDB.InvoicesByStatus(int(cms.InvoiceStatusApproved))
   144  	if err != nil {
   145  		return err
   146  	}
   147  	for _, invoice := range approvedInvoices {
   148  		_, err := p.cmsDB.PaymentsByAddress(invoice.PaymentAddress)
   149  		if err != nil {
   150  			if errors.Is(err, database.ErrInvoiceNotFound) {
   151  				payout, err := calculatePayout(invoice)
   152  				if err != nil {
   153  					return err
   154  				}
   155  				// Start listening the first day of the next month of invoice.
   156  				listenStartDate := time.Date(int(invoice.Year),
   157  					time.Month(invoice.Month+1), 0, 0, 0, 0, 0, time.UTC)
   158  				invoice.Payments = database.Payments{
   159  					InvoiceToken: invoice.Token,
   160  					Address:      strings.TrimSpace(invoice.PaymentAddress),
   161  					TimeStarted:  listenStartDate.Unix(),
   162  					Status:       cms.PaymentStatusWatching,
   163  					AmountNeeded: int64(payout.DCRTotal),
   164  				}
   165  				err = p.cmsDB.UpdateInvoice(&invoice)
   166  				if err != nil {
   167  					return err
   168  				}
   169  			}
   170  		}
   171  	}
   172  	unpaidPayments, err := p.cmsDB.PaymentsByStatus(uint(cms.PaymentStatusWatching))
   173  	if err != nil {
   174  		return err
   175  	}
   176  	for _, payments := range unpaidPayments {
   177  		payments.Address = strings.TrimSpace(payments.Address)
   178  		paid := p.checkHistoricalPayments(ctx, &payments)
   179  		if !paid {
   180  			p.addWatchAddress(payments.Address)
   181  		}
   182  	}
   183  
   184  	// Also check to make sure all paid invoices in the database have
   185  	// corresponding payment data saved in records.
   186  	paidPayments, err := p.cmsDB.PaymentsByStatus(uint(cms.PaymentStatusPaid))
   187  	if err != nil {
   188  		return err
   189  	}
   190  	for _, payments := range paidPayments {
   191  		if payments.TxIDs == "" && payments.AmountReceived == 0 {
   192  			payments.Address = strings.TrimSpace(payments.Address)
   193  			paid := p.checkHistoricalPayments(ctx, &payments)
   194  			if !paid {
   195  				log.Errorf("found payment for invoice that is set to paid "+
   196  					"no payment found %v", payments.InvoiceToken)
   197  			}
   198  		} else {
   199  			log.Debugf("payment for %v has is proper txids %v and "+
   200  				"amount received %v", payments.InvoiceToken, payments.TxIDs,
   201  				payments.AmountReceived)
   202  		}
   203  	}
   204  
   205  	return nil
   206  }
   207  
   208  // checkHistoicalPayments checks to see if a given payment has been successfully paid.
   209  // It will return TRUE if paid, otherwise false.  It utilizes the util
   210  // FetchTxsForAddressNotBefore which looks for transaction at a given address
   211  // after a certain time (in Unix seconds).
   212  func (p *Politeiawww) checkHistoricalPayments(ctx context.Context, payment *database.Payments) bool {
   213  	// Get all txs since start time of watcher
   214  	txs, err := fetchTxsForAddressNotBefore(ctx, p.params, strings.TrimSpace(payment.Address),
   215  		payment.TimeStarted, p.dcrdataHostHTTP())
   216  	if err != nil {
   217  		// XXX Some sort of 'recheck' or notice that it should do it again?
   218  		log.Errorf("error FetchTxsForAddressNotBefore for address %s: %v",
   219  			payment.Address, err)
   220  	}
   221  
   222  	txIDs := ""
   223  	// Calculate amount received
   224  	amountReceived := dcrutil.Amount(0)
   225  	log.Debugf("Reviewing transactions for address: %v", payment.Address)
   226  	for i, tx := range txs {
   227  		// Check to see if running mainnet, if so, only accept transactions
   228  		// that originate from the Treasury Subsidy.
   229  		if !p.cfg.TestNet {
   230  			found := false
   231  			for _, address := range tx.InputAddresses {
   232  				if address == mainnetSubsidyAddr {
   233  					found = true
   234  					break
   235  				}
   236  			}
   237  			if !found {
   238  				continue
   239  			}
   240  		}
   241  		log.Debugf("Transaction %v with amount %v", tx.TxID, tx.Amount)
   242  		amountReceived += dcrutil.Amount(tx.Amount)
   243  		if i == 0 {
   244  			txIDs = tx.TxID
   245  		} else {
   246  			txIDs += ", " + tx.TxID
   247  		}
   248  		if payment.TimeLastUpdated < tx.Timestamp {
   249  			payment.TimeLastUpdated = tx.Timestamp
   250  		}
   251  	}
   252  	payment.TxIDs = txIDs
   253  
   254  	log.Debugf("Amount received %v amount needed %v", int64(amountReceived),
   255  		payment.AmountNeeded)
   256  
   257  	if int64(amountReceived) == payment.AmountReceived {
   258  		// Amount received still the same so nothing to update.
   259  		return false
   260  	}
   261  
   262  	if int64(amountReceived) >= payment.AmountNeeded && amountReceived > 0 {
   263  		log.Debugf("Invoice %v paid!", payment.InvoiceToken)
   264  		payment.Status = cms.PaymentStatusPaid
   265  	}
   266  	payment.AmountReceived = int64(amountReceived)
   267  
   268  	err = p.updateInvoicePayment(ctx, payment)
   269  	if err != nil {
   270  		log.Errorf("Error updating payments information for: %v %v",
   271  			payment.Address, err)
   272  	}
   273  
   274  	if payment.Status == cms.PaymentStatusPaid {
   275  		log.Debugf("Updating invoice %v status to paid", payment.InvoiceToken)
   276  		// Update invoice status here
   277  		err := p.invoiceStatusPaid(ctx, payment.InvoiceToken, payment.InvoiceKey)
   278  		if err != nil {
   279  			log.Errorf("error updating invoice status to paid %v", err)
   280  		}
   281  		return true
   282  	}
   283  	return false
   284  }
   285  
   286  // checkPayments checks to see if a given payment has been successfully paid.
   287  // It will return TRUE if paid, otherwise false.  It utilizes the util
   288  // FetchTx which looks for transaction at a given address.
   289  func (p *Politeiawww) checkPayments(ctx context.Context, payment *database.Payments, notifiedTx string) bool {
   290  	tx, err := fetchTx(ctx, p.params, payment.Address, notifiedTx, p.dcrdataHostHTTP())
   291  	if err != nil {
   292  		log.Errorf("error FetchTxs for address %s: %v", payment.Address, err)
   293  		return false
   294  	}
   295  	if tx == nil {
   296  		log.Errorf("cannot find txid %v for address %v", notifiedTx, payment.Address)
   297  		return false
   298  	}
   299  	// Calculate amount received
   300  	amountReceived := dcrutil.Amount(0)
   301  	log.Debugf("Reviewing transactions for address: %v", payment.Address)
   302  
   303  	// Check to see if running mainnet, if so, only accept transactions
   304  	// that originate from the Treasury Subsidy.
   305  	if !p.cfg.TestNet {
   306  		for _, address := range tx.InputAddresses {
   307  			if address != mainnetSubsidyAddr {
   308  				// All input addresses should be from the subsidy address
   309  				return false
   310  			}
   311  		}
   312  	}
   313  	log.Debugf("Transaction %v with amount %v", tx.TxID, tx.Amount)
   314  	amountReceived += dcrutil.Amount(tx.Amount)
   315  
   316  	if payment.TxIDs == "" {
   317  		payment.TxIDs = tx.TxID
   318  	} else {
   319  		payment.TxIDs += ", " + tx.TxID
   320  	}
   321  
   322  	log.Debugf("Amount received %v amount needed %v", int64(amountReceived),
   323  		payment.AmountNeeded)
   324  
   325  	if int64(amountReceived) >= payment.AmountNeeded && amountReceived > 0 {
   326  		log.Debugf("Invoice %v paid!", payment.InvoiceToken)
   327  		payment.Status = cms.PaymentStatusPaid
   328  	}
   329  	payment.AmountReceived = int64(amountReceived)
   330  	payment.TimeLastUpdated = time.Now().Unix()
   331  
   332  	err = p.updateInvoicePayment(ctx, payment)
   333  	if err != nil {
   334  		log.Errorf("error updateInvoicePayment %v", err)
   335  		return false
   336  	}
   337  
   338  	if payment.Status == cms.PaymentStatusPaid {
   339  		log.Debugf("Updating invoice %v status to paid", payment.InvoiceToken)
   340  		// Update invoice status here
   341  		err := p.invoiceStatusPaid(ctx, payment.InvoiceToken, payment.InvoiceKey)
   342  		if err != nil {
   343  			log.Errorf("error updating invoice status to paid %v", err)
   344  		}
   345  		return true
   346  	}
   347  	return false
   348  }
   349  
   350  func (p *Politeiawww) updateInvoicePayment(ctx context.Context, payment *database.Payments) error {
   351  	// Create new backend invoice payment metadata
   352  	c := mdstream.InvoicePayment{
   353  		Version:        mdstream.VersionInvoicePayment,
   354  		TxIDs:          payment.TxIDs,
   355  		Timestamp:      payment.TimeLastUpdated,
   356  		AmountReceived: payment.AmountReceived,
   357  	}
   358  
   359  	blob, err := mdstream.EncodeInvoicePayment(c)
   360  	if err != nil {
   361  		return err
   362  	}
   363  
   364  	challenge, err := util.Random(pd.ChallengeSize)
   365  	if err != nil {
   366  		return err
   367  	}
   368  
   369  	pdCommand := pd.UpdateVettedMetadata{
   370  		Challenge: hex.EncodeToString(challenge),
   371  		Token:     payment.InvoiceToken,
   372  		MDAppend: []pd.MetadataStream{
   373  			{
   374  				ID:      mdstream.IDInvoicePayment,
   375  				Payload: string(blob),
   376  			},
   377  		},
   378  	}
   379  	responseBody, err := p.makeRequest(ctx, http.MethodPost,
   380  		pd.UpdateVettedMetadataRoute, pdCommand)
   381  	if err != nil {
   382  		return err
   383  	}
   384  
   385  	var pdReply pd.UpdateVettedMetadataReply
   386  	err = json.Unmarshal(responseBody, &pdReply)
   387  	if err != nil {
   388  		return fmt.Errorf("Could not unmarshal UpdateVettedMetadataReply: %v",
   389  			err)
   390  	}
   391  
   392  	// Verify the UpdateVettedMetadata challenge.
   393  	err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response)
   394  	if err != nil {
   395  		return err
   396  	}
   397  
   398  	err = p.cmsDB.UpdatePayments(payment)
   399  	if err != nil {
   400  		log.Errorf("Error updating payments information for: %v %v",
   401  			payment.Address, err)
   402  	}
   403  	return nil
   404  }
   405  func (p *Politeiawww) invoiceStatusPaid(ctx context.Context, token, key string) error {
   406  	dbInvoice, err := p.cmsDB.InvoiceByKey(key)
   407  	if err != nil {
   408  		if errors.Is(err, database.ErrInvoiceNotFound) {
   409  			err = www.UserError{
   410  				ErrorCode: cms.ErrorStatusInvoiceNotFound,
   411  			}
   412  		}
   413  		return err
   414  	}
   415  
   416  	// Create the change record.
   417  	c := mdstream.InvoiceStatusChange{
   418  		Version:   mdstream.VersionInvoiceStatusChange,
   419  		Timestamp: time.Now().Unix(),
   420  		NewStatus: cms.InvoiceStatusPaid,
   421  		Reason:    "Invoice watcher found payment transactions.",
   422  	}
   423  
   424  	blob, err := mdstream.EncodeInvoiceStatusChange(c)
   425  	if err != nil {
   426  		return err
   427  	}
   428  
   429  	challenge, err := util.Random(pd.ChallengeSize)
   430  	if err != nil {
   431  		return err
   432  	}
   433  
   434  	pdCommand := pd.UpdateVettedMetadata{
   435  		Challenge: hex.EncodeToString(challenge),
   436  		Token:     token,
   437  		MDAppend: []pd.MetadataStream{
   438  			{
   439  				ID:      mdstream.IDInvoiceStatusChange,
   440  				Payload: string(blob),
   441  			},
   442  		},
   443  	}
   444  	responseBody, err := p.makeRequest(ctx, http.MethodPost,
   445  		pd.UpdateVettedMetadataRoute, pdCommand)
   446  	if err != nil {
   447  		return err
   448  	}
   449  
   450  	var pdReply pd.UpdateVettedMetadataReply
   451  	err = json.Unmarshal(responseBody, &pdReply)
   452  	if err != nil {
   453  		return fmt.Errorf("Could not unmarshal UpdateVettedMetadataReply: %v",
   454  			err)
   455  	}
   456  
   457  	// Verify the UpdateVettedMetadata challenge.
   458  	err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response)
   459  	if err != nil {
   460  		return err
   461  	}
   462  
   463  	// Update the database with the metadata changes.
   464  	dbInvoice.Changes = append(dbInvoice.Changes, database.InvoiceChange{
   465  		Timestamp: c.Timestamp,
   466  		NewStatus: c.NewStatus,
   467  		Reason:    c.Reason,
   468  	})
   469  	dbInvoice.StatusChangeReason = c.Reason
   470  	dbInvoice.Status = c.NewStatus
   471  
   472  	err = p.cmsDB.UpdateInvoice(dbInvoice)
   473  	if err != nil {
   474  		return err
   475  	}
   476  
   477  	cmsUser, err := p.getCMSUserByID(dbInvoice.UserID)
   478  	if err != nil {
   479  		return err
   480  	}
   481  
   482  	if cmsUser.ContractorType == cms.ContractorTypeTemp {
   483  		// Update Temp User's Contractor Type to Deactivated
   484  		cmsUserID, err := uuid.Parse(cmsUser.ID)
   485  		if err != nil {
   486  			return err
   487  		}
   488  		uu := user.UpdateCMSUser{
   489  			ID:             cmsUserID,
   490  			ContractorType: int(cms.ContractorTypeTempDeactivated),
   491  		}
   492  
   493  		payload, err := user.EncodeUpdateCMSUser(uu)
   494  		if err != nil {
   495  			return err
   496  		}
   497  		pc := user.PluginCommand{
   498  			ID:      user.CMSPluginID,
   499  			Command: user.CmdUpdateCMSUser,
   500  			Payload: string(payload),
   501  		}
   502  		_, err = p.db.PluginExec(pc)
   503  		if err != nil {
   504  			return err
   505  		}
   506  	}
   507  	return nil
   508  }