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 }